From 2810b4e01666f8fa633529c850742ef90daf38f5 Mon Sep 17 00:00:00 2001 From: Alistair King Date: Mon, 23 Sep 2019 20:31:38 +0200 Subject: [PATCH 1/6] Ensure ARTEMIS config file is not clobbered on pod restart (#232) Previously when the backend pod restarted it would overwrite the config file in persistent storage with the one from the configmap (i.e., the default). The `-u` option to `cp` doesn't seem to be enough to prevent an overwrite (presumably the modification time of the file from the configmap cannot be trusted). Using both `-u` and the `-n` (no-clobber) option seems to work and prevent the persisted config file from being overwritten. --- artemis-chart/templates/backend-deployment.yaml | 2 +- artemis-chart/templates/monitor-deployment.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/artemis-chart/templates/backend-deployment.yaml b/artemis-chart/templates/backend-deployment.yaml index 75fdd3422..b593db61f 100644 --- a/artemis-chart/templates/backend-deployment.yaml +++ b/artemis-chart/templates/backend-deployment.yaml @@ -26,7 +26,7 @@ spec: - mountPath: /pvc name: backend-pvc subPath: configs - command: ['sh', '-c', 'cp -u /configmaps/config.yaml /configmaps/logging.yaml /configmaps/services.conf /pvc/'] + command: ['sh', '-c', 'cp -n -u /configmaps/config.yaml /configmaps/logging.yaml /configmaps/services.conf /pvc/'] - name: wait-for-rmq image: busybox command: ['sh', '-c', 'until nc -z {{ .Values.rabbitmqHost }} {{ .Values.rabbitmqPort}}; do echo waiting for services; sleep 10; done;'] diff --git a/artemis-chart/templates/monitor-deployment.yaml b/artemis-chart/templates/monitor-deployment.yaml index 0c761e7f6..ed17e42d2 100644 --- a/artemis-chart/templates/monitor-deployment.yaml +++ b/artemis-chart/templates/monitor-deployment.yaml @@ -26,7 +26,7 @@ spec: - mountPath: /pvc name: monitor-pvc subPath: configs - command: ['sh', '-c', 'cp -u /configmaps/logging.yaml /configmaps/mon-services.conf /pvc/'] + command: ['sh', '-c', 'cp -n -u /configmaps/logging.yaml /configmaps/mon-services.conf /pvc/'] - name: wait-for-service image: busybox command: ['sh', '-c', 'until nc -z {{ .Values.rabbitmqHost }} {{ .Values.rabbitmqPort}}; do echo waiting for services; sleep 10; done;'] From abd4e6befa1e0634a98eec9e1e5bcd66f7af2cde Mon Sep 17 00:00:00 2001 From: vkotronis Date: Mon, 23 Sep 2019 21:59:03 +0300 Subject: [PATCH 2/6] updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 413dd6862..f063aafb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Fixed - Support for millions of prefixes in configuration file +- Ensure ARTEMIS config file is not clobbered on pod restart ### Removed - ipaddress requirement from frontend (not needed) From ac9699d819adfdcf3bd50fa756e4482b1ce75740 Mon Sep 17 00:00:00 2001 From: vkotronis Date: Tue, 24 Sep 2019 11:32:37 +0300 Subject: [PATCH 3/6] fixing backup config for failed AS_SET resolutions --- backend/testing/configs/config3.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/backend/testing/configs/config3.yaml b/backend/testing/configs/config3.yaml index 2bb908cd3..a42032c9e 100644 --- a/backend/testing/configs/config3.yaml +++ b/backend/testing/configs/config3.yaml @@ -27,6 +27,12 @@ prefixes: - 10.0.20.0/24 test_as_set_24: &test_as_set_24 - 10.0.30.0/24 + test_ipv6_benign: &test_ipv6_benign + - 2001:db8:abcd:10::/64 + test_ipv6_exact: &test_ipv6_exact + - 2001:db8:abcd:11::/64 + test_ipv6_sub: &test_ipv6_sub + - 2001:db8:abcd:12::/64 asns: 8_origin: &8_origin 1 @@ -48,6 +54,10 @@ asns: 444 comm_test_origin: &comm_test_origin 555 + v6_origins: &v6_origins + - 777 + - 888 + - 999 rules: - prefixes: - *8_prefix @@ -144,3 +154,13 @@ rules: - *test_as_set_24 origin_asns: - 6777 +- prefixes: + - *test_ipv6_benign + - *test_ipv6_exact + - *test_ipv6_sub + origin_asns: + - *v6_origins + neighbors: + '*' + mitigation: + manual From 2a4de431f1e57e4e52eda4f76c37a28c271413f1 Mon Sep 17 00:00:00 2001 From: vkotronis Date: Wed, 25 Sep 2019 22:43:01 +0300 Subject: [PATCH 4/6] Reset the modules to last known state upon restart (#238) * added db table and hasura view for intended_process_states * initializing intended monitor, detection and mitigation * frontend graphql query working + db module auto-conf --- .env | 2 +- artemis-chart/templates/configmap.yaml | 2 +- artemis-chart/values.yaml | 2 +- backend/core/database.py | 43 ++++++++++++++- backend/core/utils/__init__.py | 55 +++++++++++++++++++ backend/hasura_init.json | 53 ++++++++++++++++++ .../migrations/scripts/migration_18.sql | 6 ++ backend/migrate/migrations/target_steps.json | 6 ++ backend/testing/db/data/tables.sql | 9 ++- frontend/webapp/core/modules.py | 51 +++++++++++++++++ frontend/webapp/utils/__init__.py | 4 +- other/db/data/tables.sql | 9 ++- 12 files changed, 234 insertions(+), 8 deletions(-) create mode 100644 backend/migrate/migrations/scripts/migration_18.sql diff --git a/.env b/.env index 014b700c5..0941f38a9 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ # Docker specific configs # use only letters and numbers for the project name COMPOSE_PROJECT_NAME=artemis -DB_VERSION=17 +DB_VERSION=18 GUI_ENABLED=true SYSTEM_VERSION=latest HISTORIC=false diff --git a/artemis-chart/templates/configmap.yaml b/artemis-chart/templates/configmap.yaml index 519e105e0..9bf7dfb2b 100644 --- a/artemis-chart/templates/configmap.yaml +++ b/artemis-chart/templates/configmap.yaml @@ -19,7 +19,7 @@ data: risId: {{ .Values.risId | default "8522" | quote }} dbHost: {{ .Values.dbHost | default "postgres" | quote }} dbPort: {{ .Values.dbPort | default "5432" | quote }} - dbVersion: {{ .Values.dbVersion | default "17" | quote }} + dbVersion: {{ .Values.dbVersion | default "18" | quote }} dbName: {{ .Values.dbName | default "artemis_db" | quote }} dbUser: {{ .Values.dbUser | default "artemis_user" | quote }} dbSchema: {{ .Values.dbSchema | default "public" | quote }} diff --git a/artemis-chart/values.yaml b/artemis-chart/values.yaml index a1e27f7ab..e6bc6d47b 100644 --- a/artemis-chart/values.yaml +++ b/artemis-chart/values.yaml @@ -20,7 +20,7 @@ risId: 8522 # database dbHost: postgres dbPort: 5432 -dbVersion: 17 +dbVersion: 18 dbName: artemis_db dbUser: artemis_user dbPass: Art3m1s diff --git a/backend/core/database.py b/backend/core/database.py index 9cf73d39d..931d10da1 100644 --- a/backend/core/database.py +++ b/backend/core/database.py @@ -23,6 +23,7 @@ from utils import get_ro_cursor from utils import get_wo_cursor from utils import HISTORIC +from utils import ModulesState from utils import MON_SUPERVISOR_URI from utils import ping_redis from utils import purge_redis_eph_pers_keys @@ -111,6 +112,26 @@ def __init__(self, connection, ro_conn, wo_conn): except Exception: log.exception("exception") + try: + query = ( + "INSERT INTO intended_process_states (name, running) " + "VALUES (%s, %s) ON CONFLICT(name) DO NOTHING" + ) + + for ctx in {BACKEND_SUPERVISOR_URI, MON_SUPERVISOR_URI}: + server = ServerProxy(ctx) + processes = [ + (x["name"], False) + for x in server.supervisor.getAllProcessInfo() + if x["name"] in ["monitor", "detection", "mitigation"] + ] + + with get_wo_cursor(self.wo_conn) as db_cur: + psycopg2.extras.execute_batch(db_cur, query, processes) + + except Exception: + log.exception("exception") + # redis db self.redis = redis.Redis(host=REDIS_HOST, port=REDIS_PORT) ping_redis(self.redis) @@ -393,6 +414,23 @@ def get_consumers(self, Consumer, channel): ), ] + def set_modules_to_intended_state(self): + try: + query = "SELECT name, running FROM intended_process_states" + + with get_ro_cursor(self.ro_conn) as db_cur: + db_cur.execute(query) + entries = db_cur.fetchall() + modules_state = ModulesState() + for entry in entries: + # entry[0] --> module name, entry[1] --> intended state + # start only intended modules, do not stop running ones! + if entry[1]: + log.info("Setting {} to start state.".format(entry[0])) + modules_state.call(entry[0], "start") + except Exception: + log.exception("exception") + def config_request_rpc(self): self.correlation_id = uuid() callback_queue = Queue( @@ -718,7 +756,7 @@ def find_best_prefix_match(self, prefix): return None def handle_config_notify(self, message): - log.info("Reconfiguring database...") + log.info("Reconfiguring database due to conf update...") log.debug("Message: {}\npayload: {}".format(message, message.payload)) config = message.payload @@ -746,7 +784,7 @@ def handle_config_notify(self, message): log.info("Database initiated, configured and running.") def handle_config_request_reply(self, message): - log.info("Reconfiguring database...") + log.info("Configuring database for the first time...") log.debug("Message: {}\npayload: {}".format(message, message.payload)) config = message.payload @@ -776,6 +814,7 @@ def handle_config_request_reply(self, message): log.debug("database config is up-to-date") except Exception: log.exception("{}".format(config)) + self.set_modules_to_intended_state() log.info("Database initiated, configured and running.") diff --git a/backend/core/utils/__init__.py b/backend/core/utils/__init__.py index f2772bd9b..1f382d2cc 100644 --- a/backend/core/utils/__init__.py +++ b/backend/core/utils/__init__.py @@ -8,6 +8,7 @@ from contextlib import contextmanager from ipaddress import ip_network as str2ip from logging.handlers import SMTPHandler +from xmlrpc.client import ServerProxy import psycopg2 import requests @@ -135,6 +136,60 @@ def get_logger(path="/etc/artemis/logging.yaml"): log = get_logger() +class ModulesState: + def __init__(self): + self.backend_server = ServerProxy(BACKEND_SUPERVISOR_URI) + self.mon_server = ServerProxy(MON_SUPERVISOR_URI) + + def call(self, module, action): + try: + if module == "all": + if action == "start": + for ctx in {self.backend_server, self.mon_server}: + ctx.supervisor.startAllProcesses() + elif action == "stop": + for ctx in {self.backend_server, self.mon_server}: + ctx.supervisor.stopAllProcesses() + else: + ctx = self.backend_server + if module == "monitor": + ctx = self.mon_server + + if action == "start": + modules = self.is_any_up_or_running(module, up=False) + for mod in modules: + ctx.supervisor.startProcess(mod) + + elif action == "stop": + modules = self.is_any_up_or_running(module) + for mod in modules: + ctx.supervisor.stopProcess(mod) + + except Exception: + log.exception("exception") + + def is_any_up_or_running(self, module, up=True): + ctx = self.backend_server + if module == "monitor": + ctx = self.mon_server + + try: + if up: + return [ + "{}:{}".format(x["group"], x["name"]) + for x in ctx.supervisor.getAllProcessInfo() + if x["group"] == module and (x["state"] == 20 or x["state"] == 10) + ] + return [ + "{}:{}".format(x["group"], x["name"]) + for x in ctx.supervisor.getAllProcessInfo() + if x["group"] == module and (x["state"] != 20 and x["state"] != 10) + ] + except Exception: + log.exception("exception") + return False + + @contextmanager def get_ro_cursor(conn): with conn.cursor() as curr: diff --git a/backend/hasura_init.json b/backend/hasura_init.json index 283a27696..5c72cdae6 100644 --- a/backend/hasura_init.json +++ b/backend/hasura_init.json @@ -54,6 +54,59 @@ "delete_permissions": [], "event_triggers": [] }, + { + "table": "view_intended_process_states", + "object_relationships": [], + "array_relationships": [], + "insert_permissions": [], + "select_permissions": [ + { + "role": "user", + "comment": null, + "permission": { + "allow_aggregations": true, + "columns": [ + "name", + "running" + ], + "filter": {} + } + } + ], + "update_permissions": [ + { + "role": "user", + "comment": null, + "permission": { + "columns": [ + "name", + "running" + ], + "filter": { + "$or": [ + { + "name": { + "$eq": "monitor" + } + }, + { + "name": { + "$eq": "detection" + } + }, + { + "name": { + "$eq": "mitigation" + } + } + ] + } + } + } + ], + "delete_permissions": [], + "event_triggers": [] + }, { "table": "view_bgpupdates", "object_relationships": [], diff --git a/backend/migrate/migrations/scripts/migration_18.sql b/backend/migrate/migrations/scripts/migration_18.sql new file mode 100644 index 000000000..b095ef78f --- /dev/null +++ b/backend/migrate/migrations/scripts/migration_18.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS intended_process_states ( + name VARCHAR (32) UNIQUE, + running BOOLEAN DEFAULT FALSE +); + +CREATE OR REPLACE VIEW view_intended_process_states AS SELECT * FROM intended_process_states; diff --git a/backend/migrate/migrations/target_steps.json b/backend/migrate/migrations/target_steps.json index 958b61222..7fd06c607 100644 --- a/backend/migrate/migrations/target_steps.json +++ b/backend/migrate/migrations/target_steps.json @@ -101,6 +101,12 @@ "db_version": "17", "description": "Added community_annotation column in hijacks table", "file": "migration_17.sql" + }, + "18": { + "id": "18", + "db_version": "18", + "description": "Added intended process states table in db", + "file": "migration_18.sql" } } } diff --git a/backend/testing/db/data/tables.sql b/backend/testing/db/data/tables.sql index 491c745a2..fbbbc3d7a 100644 --- a/backend/testing/db/data/tables.sql +++ b/backend/testing/db/data/tables.sql @@ -22,7 +22,7 @@ CREATE TRIGGER db_details_no_delete BEFORE DELETE ON db_details FOR EACH ROW EXECUTE PROCEDURE db_version_no_delete(); -INSERT INTO db_details (version, upgraded_on) VALUES (17, now()); +INSERT INTO db_details (version, upgraded_on) VALUES (18, now()); CREATE TABLE IF NOT EXISTS bgp_updates ( key VARCHAR ( 32 ) NOT NULL, @@ -201,6 +201,11 @@ CREATE TABLE IF NOT EXISTS process_states ( timestamp TIMESTAMP default current_timestamp ); +CREATE TABLE IF NOT EXISTS intended_process_states ( + name VARCHAR (32) UNIQUE, + running BOOLEAN DEFAULT FALSE +); + CREATE OR REPLACE FUNCTION update_timestamp() RETURNS TRIGGER AS $$ BEGIN @@ -215,6 +220,8 @@ FOR EACH ROW EXECUTE PROCEDURE update_timestamp(); CREATE OR REPLACE VIEW view_processes AS SELECT * FROM process_states; +CREATE OR REPLACE VIEW view_intended_process_states AS SELECT * FROM intended_process_states; + CREATE OR REPLACE VIEW view_db_details AS SELECT version, upgraded_on FROM db_details; CREATE FUNCTION search_bgpupdates_as_path(as_paths BIGINT[]) diff --git a/frontend/webapp/core/modules.py b/frontend/webapp/core/modules.py index f0bfd4559..bb966bf7a 100644 --- a/frontend/webapp/core/modules.py +++ b/frontend/webapp/core/modules.py @@ -1,10 +1,16 @@ +import json import logging import time from xmlrpc.client import ServerProxy +import requests +from flask_jwt_extended import create_access_token +from flask_security import current_user from webapp.utils import BACKEND_SUPERVISOR_URI +from webapp.utils import GRAPHQL_URI from webapp.utils import MON_SUPERVISOR_URI + log = logging.getLogger("webapp_logger") intervals = ( @@ -15,6 +21,20 @@ ("S", 1), ) +intended_process_states_mutation = """ +mutation updateIntendedProcessStates($name: String, $running: Boolean) { + update_view_intended_process_states(where: {name: {_eq: $name}}, _set: {running: $running}) { + affected_rows + returning { + name + running + } + } +} +""" + +user_controlled_modules = ["monitor", "detection", "mitigation"] + def display_time(seconds, granularity=2): result = [] @@ -53,11 +73,20 @@ def call(self, module, action): modules = self.is_any_up_or_running(module, up=False) for mod in modules: ctx.supervisor.startProcess(mod) + if module in user_controlled_modules: + self.update_intended_process_states( + name=module, running=True + ) elif action == "stop": modules = self.is_any_up_or_running(module) for mod in modules: ctx.supervisor.stopProcess(mod) + if module in user_controlled_modules: + self.update_intended_process_states( + name=module, running=False + ) + except Exception: log.exception("exception") @@ -113,3 +142,25 @@ def get_response_all(self): def get_response_formatted_all(self): return self.get_response_all() + + def update_intended_process_states(self, name, running=False): + try: + access_token = create_access_token(identity=current_user) + graqphql_request_headers = { + "Content-Type": "application/json; charset=utf-8", + "Authorization": "Bearer {}".format(access_token), + } + graqphql_request_payload = json.dumps( + { + "variables": {"name": name, "running": running}, + "operationName": "updateIntendedProcessStates", + "query": intended_process_states_mutation, + } + ) + requests.post( + url=GRAPHQL_URI, + headers=graqphql_request_headers, + data=graqphql_request_payload, + ) + except Exception: + log.exception("exception") diff --git a/frontend/webapp/utils/__init__.py b/frontend/webapp/utils/__init__.py index 7f8024a5d..0e0f27a0d 100644 --- a/frontend/webapp/utils/__init__.py +++ b/frontend/webapp/utils/__init__.py @@ -11,17 +11,19 @@ BACKEND_SUPERVISOR_PORT = os.getenv("BACKEND_SUPERVISOR_PORT", 9001) MON_SUPERVISOR_HOST = os.getenv("MON_SUPERVISOR_HOST", "monitor") MON_SUPERVISOR_PORT = os.getenv("MON_SUPERVISOR_PORT", 9001) - RABBITMQ_URI = "amqp://{}:{}@{}:{}//".format( RABBITMQ_USER, RABBITMQ_PASS, RABBITMQ_HOST, RABBITMQ_PORT ) + BACKEND_SUPERVISOR_URI = "http://{}:{}/RPC2".format( BACKEND_SUPERVISOR_HOST, BACKEND_SUPERVISOR_PORT ) MON_SUPERVISOR_URI = "http://{}:{}/RPC2".format( MON_SUPERVISOR_HOST, MON_SUPERVISOR_PORT ) + API_URI = "http://{}:{}".format(API_HOST, API_PORT) +GRAPHQL_URI = "http://graphql:8080/v1alpha1/graphql" def flatten(items, seqtypes=(list, tuple)): diff --git a/other/db/data/tables.sql b/other/db/data/tables.sql index e5e0bdeba..40aa4570f 100644 --- a/other/db/data/tables.sql +++ b/other/db/data/tables.sql @@ -22,7 +22,7 @@ CREATE TRIGGER db_details_no_delete BEFORE DELETE ON db_details FOR EACH ROW EXECUTE PROCEDURE db_version_no_delete(); -INSERT INTO db_details (version, upgraded_on) VALUES (17, now()); +INSERT INTO db_details (version, upgraded_on) VALUES (18, now()); CREATE TABLE IF NOT EXISTS bgp_updates ( key VARCHAR ( 32 ) NOT NULL, @@ -197,6 +197,11 @@ CREATE TABLE IF NOT EXISTS process_states ( timestamp TIMESTAMP default current_timestamp ); +CREATE TABLE IF NOT EXISTS intended_process_states ( + name VARCHAR (32) UNIQUE, + running BOOLEAN DEFAULT FALSE +); + CREATE OR REPLACE FUNCTION update_timestamp() RETURNS TRIGGER AS $$ BEGIN @@ -211,6 +216,8 @@ FOR EACH ROW EXECUTE PROCEDURE update_timestamp(); CREATE OR REPLACE VIEW view_processes AS SELECT * FROM process_states; +CREATE OR REPLACE VIEW view_intended_process_states AS SELECT * FROM intended_process_states; + CREATE OR REPLACE VIEW view_db_details AS SELECT version, upgraded_on FROM db_details; CREATE FUNCTION search_bgpupdates_as_path(as_paths BIGINT[]) From cb4aa65fb67fae88eb780c17d9a0f9ff4f873537 Mon Sep 17 00:00:00 2001 From: vkotronis Date: Wed, 25 Sep 2019 23:23:48 +0300 Subject: [PATCH 5/6] updated CHANGELOG + minor fix for group module reinstate --- CHANGELOG.md | 1 + backend/core/database.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f063aafb0..f90743572 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - IPv6 tests (backend testing) - PR labeler (GitHub actions) +- Reinstating intended modules on ARTEMIS startup ### Changed - py-radix, substituted with pytricia tree diff --git a/backend/core/database.py b/backend/core/database.py index 931d10da1..cd040645e 100644 --- a/backend/core/database.py +++ b/backend/core/database.py @@ -121,9 +121,9 @@ def __init__(self, connection, ro_conn, wo_conn): for ctx in {BACKEND_SUPERVISOR_URI, MON_SUPERVISOR_URI}: server = ServerProxy(ctx) processes = [ - (x["name"], False) + (x["group"], False) for x in server.supervisor.getAllProcessInfo() - if x["name"] in ["monitor", "detection", "mitigation"] + if x["group"] in ["monitor", "detection", "mitigation"] ] with get_wo_cursor(self.wo_conn) as db_cur: From 61d7ba70a33302ce9c0b8c299d1adfa0f3248307 Mon Sep 17 00:00:00 2001 From: vkotronis Date: Wed, 25 Sep 2019 23:29:39 +0300 Subject: [PATCH 6/6] minor quick-fix --- backend/core/database.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/core/database.py b/backend/core/database.py index cd040645e..6d9b8bbdf 100644 --- a/backend/core/database.py +++ b/backend/core/database.py @@ -424,9 +424,12 @@ def set_modules_to_intended_state(self): modules_state = ModulesState() for entry in entries: # entry[0] --> module name, entry[1] --> intended state - # start only intended modules, do not stop running ones! + # start only intended modules (after making sure they are stopped + # to avoid stale entries) if entry[1]: log.info("Setting {} to start state.".format(entry[0])) + modules_state.call(entry[0], "stop") + time.sleep(1) modules_state.call(entry[0], "start") except Exception: log.exception("exception")