From 144c90aa33203d03ddca61269e50dd81f98e5e88 Mon Sep 17 00:00:00 2001 From: Lorena Goldoni <33703137+Lorygold@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:07:17 +0100 Subject: [PATCH 01/15] Cleaned venv from useless packages (#103) * Cleaned venv from useless packages * Added pytz required in celery_beat * fix * Updated CHANGELOG.md * Registered UserAdmin for authentication app in admin.py --- CHANGELOG.md | 9 +++++++++ buffalogs/authentication/admin.py | 8 +++++++- buffalogs/authentication/models.py | 3 --- buffalogs/celerybeat-schedule | Bin 16384 -> 16384 bytes buffalogs/requirements.txt | 1 + buffalogs/requirements_dev.txt | 3 ++- 6 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1be8c2..4fc3670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,13 @@ ## 1.2.x +### 1.2.12 +#### Bugfix +* Cleaned venv from useless packages +* Added pytz in requirements because it's needed by celery_beat +* Registered UserAdmin in authentication +### 1.2.11 +#### Bugfix +* Fixed the update of the login.updated field +* Added logging for the clear_models_periodically function ### 1.2.10 #### Changes * Added settings into the Config model (instead of into the settings.py file) diff --git a/buffalogs/authentication/admin.py b/buffalogs/authentication/admin.py index 8c38f3f..1a5fc47 100644 --- a/buffalogs/authentication/admin.py +++ b/buffalogs/authentication/admin.py @@ -1,3 +1,9 @@ from django.contrib import admin -# Register your models here. +from .models import User + + +@admin.register(User) +class UserAdmin(admin.ModelAdmin): + list_display = ("id", "username", "email", "is_staff", "is_verified", "created_at", "updated_at", "avatar") + search_fields = ("id", "username", "email", "avatar") diff --git a/buffalogs/authentication/models.py b/buffalogs/authentication/models.py index 2cf02e1..37e68d0 100644 --- a/buffalogs/authentication/models.py +++ b/buffalogs/authentication/models.py @@ -49,6 +49,3 @@ def __str__(self): def tokens(self): refresh = RefreshToken.for_user(self) return {"refresh": str(refresh), "access": str(refresh.access_token)} - - -# Create your models here. diff --git a/buffalogs/celerybeat-schedule b/buffalogs/celerybeat-schedule index 4073711d1d65984295abd0c2eef256af9088f591..e965372639b5d7f9ac219447d095395d5ef1e18d 100644 GIT binary patch delta 565 zcmZo@U~Fh$T%f?nGMQ09Tx%X90~q)jK+hsI@sqVF@qqWn+jC zJCJ&}SyqpeiP>1sY_q1pVMgxMypp2K)Zzw~saGdQ8WcspU>4=$;AdUAc1jONQfX#R zNoHR0lpgl<)RM%KlA{37XQw z92(+0#hVet24Qce$?pu*S*0_8aI%!4GNbZjJ3~da9+ra2lBy{^Eb$=alc#7xRWoEr zX?V+l6oWOC80u;pXBcO2ICHT>omlW@N`@%dWRTh(MuRCmj3!f{>dzP|AbIUE&};US zlMO^C&o(dsdy$KQQ7M4xUStM)Q6>WjfnHQ{?cpgX%1=%$E{@O1PX`7ge*lE*1LTDy z7H3b%;Dq@{nO$-BlngPje;6~QL4F6D12hTbcf$-rgj#-9kl!I{8Gvfxeh2H=e8w<> F3jiulwebJ| delta 70 zcmZo@U~Fh$T%f?nG?`IBoJ)j{0Sw+4PBiq|oTIRWm)Fk#BE$})rf-(j<78qs(lg(z RX>gcv^9fT6u8j_Nm;n)f5HtV) diff --git a/buffalogs/requirements.txt b/buffalogs/requirements.txt index 221b0b3..2d27d35 100644 --- a/buffalogs/requirements.txt +++ b/buffalogs/requirements.txt @@ -22,6 +22,7 @@ pygal>=3.0.0 pygal-maps-world>=1.0.2 python-dateutil>=2.8.2 python-dotenv>=0.21.0 +pytz>=2024.1 PyYAML>=6.0 urllib3>=1.26.12 uWSGI>=2.0.21 diff --git a/buffalogs/requirements_dev.txt b/buffalogs/requirements_dev.txt index 8128f2a..eb8ace9 100644 --- a/buffalogs/requirements_dev.txt +++ b/buffalogs/requirements_dev.txt @@ -1,4 +1,5 @@ pre-commit==2.21.0 mypy-extensions>=0.4.3 flake8>=3.8.4 -flake8-django>=1.1.5 \ No newline at end of file +flake8-django>=1.1.5 +pipdeptree>=2.16.0 \ No newline at end of file From 7cb3f32a7fbeaa6828960867635edb417d4e1d44 Mon Sep 17 00:00:00 2001 From: Lorena Goldoni <33703137+Lorygold@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:12:41 +0100 Subject: [PATCH 02/15] Configuration panel (#104) * [refactor] Added enums in new costants.py file for "choices" fields in the models * [refactor] Added default config values in the django settings (certego.py file) * Added default configs in settings * Updated CHANGELOG.md * [test-view] Ignore alerts order for views return API * Fixed linters paths * Changed "null=True" to "blank=True" for Charfields * changes for flake8 linter * Revoed E231 flake8 rule for certego.py file * removed comment * Updated CHANGELOG.md * [CI] Updated to compose v2 * fix --- .github/actions/services/action.yml | 4 +- .github/configurations/python_linters/.flake8 | 6 +- CHANGELOG.md | 8 + CONTRIBUTING.md | 6 +- buffalogs/buffalogs/settings/certego.py | 11 + buffalogs/impossible_travel/constants.py | 82 ++++++++ ..._filter_type_alert_is_filtered_and_more.py | 188 ++++++++++++++++++ buffalogs/impossible_travel/models.py | 105 +++++++--- .../modules/impossible_travel.py | 3 +- .../modules/login_from_new_country.py | 3 +- .../modules/login_from_new_device.py | 9 +- buffalogs/impossible_travel/tasks.py | 19 +- .../tests/test_impossible_travel.py | 2 +- .../tests/test_login_from_new_country.py | 2 +- .../tests/test_login_from_new_device.py | 2 +- .../impossible_travel/tests/test_tasks.py | 47 ++--- .../impossible_travel/tests/test_views.py | 25 +-- 17 files changed, 429 insertions(+), 93 deletions(-) create mode 100644 buffalogs/impossible_travel/constants.py create mode 100644 buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py diff --git a/.github/actions/services/action.yml b/.github/actions/services/action.yml index c48dd60..44bdd44 100644 --- a/.github/actions/services/action.yml +++ b/.github/actions/services/action.yml @@ -154,9 +154,9 @@ runs: shell: bash - - name: Execute docker-compose up + - name: Execute docker compose up run: | - CLI=docker-compose + CLI="docker compose" if [[ ${{ inputs.use_postgres }} != 'false' ]]; then CLI="${CLI} -f postgres.yml" fi diff --git a/.github/configurations/python_linters/.flake8 b/.github/configurations/python_linters/.flake8 index 4fe5917..3707569 100644 --- a/.github/configurations/python_linters/.flake8 +++ b/.github/configurations/python_linters/.flake8 @@ -16,4 +16,8 @@ ignore = exclude = */migrations/*, - Dockerfile \ No newline at end of file + Dockerfile + +per-file-ignores = + # imported but unused + certego.py: E231 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fc3670..cb753b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 1.3.x +### 1.3.0 +#### Feature +* Added configuration panel in order to set custom preferences +### Changes +* Set default settings values in the *settings.certego.py* file +* Moved Enums into *costants.py* file + ## 1.2.x ### 1.2.12 #### Bugfix diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a3e37d..7b6d9ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,15 +55,15 @@ How to create and submit a PR: If you didn't install pre-commit, it is necessary to run linters manually: * Flake8 ```bash - flake8 . --show-source --config ../.github/configurations/.flake8 + flake8 . --show-source --config ../.github/configurations/python_linters/.flake8 ``` * Black ```bash - black --config ../.github/configurations/.black . + black --config ../.github/configurations/python_linters/.black . ``` * Isort ```bash - isort --sp ../.github/configurations/.isort.cfg --profile black . + isort --sp ../.github/configurations/python_linters/.isort.cfg --profile black . ``` 3. **IF** your changes include differences in the template view, **include sceenshots of the before and after**. diff --git a/buffalogs/buffalogs/settings/certego.py b/buffalogs/buffalogs/settings/certego.py index 66d2ec0..ec4919c 100644 --- a/buffalogs/buffalogs/settings/certego.py +++ b/buffalogs/buffalogs/settings/certego.py @@ -15,6 +15,17 @@ CERTEGO_BUFFALOGS_POSTGRES_PORT = os.environ.get("BUFFALOGS_POSTGRES_PORT", "5432") CERTEGO_BUFFALOGS_ELASTIC_INDEX = os.environ.get("BUFFALOGS_ELASTIC_INDEX", "weblog-*,cloud-*,fw-proxy-*,filebeat-*") CERTEGO_BUFFALOGS_SECRET_KEY = os.environ.get("BUFFALOGS_SECRET_KEY", "django-insecure-am9z-fi-x*aqxlb-@abkhb@pu!0da%0a77h%-8d(dwzrrktwhu") +CERTEGO_BUFFALOGS_IGNORED_USERS = ["Not Available", "N/A"] +CERTEGO_BUFFALOGS_ENABLED_USERS = [] +CERTEGO_BUFFALOGS_ALLOWED_COUNTRIES = [] +CERTEGO_BUFFALOGS_IGNORED_IPS = ["127.0.0.1"] +CERTEGO_BUFFALOGS_VIP_USERS = [] +CERTEGO_BUFFALOGS_DISTANCE_KM_ACCEPTED = 100 +CERTEGO_BUFFALOGS_VEL_TRAVEL_ACCEPTED = 300 +CERTEGO_BUFFALOGS_USER_MAX_DAYS = 60 +CERTEGO_BUFFALOGS_LOGIN_MAX_DAYS = 30 +CERTEGO_BUFFALOGS_ALERT_MAX_DAYS = 30 +CERTEGO_BUFFALOGS_IP_MAX_DAYS = 30 if CERTEGO_BUFFALOGS_ENVIRONMENT == ENVIRONMENT_DOCKER: diff --git a/buffalogs/impossible_travel/constants.py b/buffalogs/impossible_travel/constants.py new file mode 100644 index 0000000..2fb9d7a --- /dev/null +++ b/buffalogs/impossible_travel/constants.py @@ -0,0 +1,82 @@ +from enum import Enum + + +class UserRiskScoreType(Enum): + """Possible types of user risk scores, based on number of alerts that they have triggered + + * No risk: the user has triggered 0 alerts + * Low: the user has triggered 1 or 2 alerts + * Medium: the user has triggered 3 or 4 alerts + * High: the user has triggered more than 4 alerts + """ + + NO_RISK = "No risk" + LOW = "Low" + MEDIUM = "Medium" + HIGH = "High" + + @classmethod + def choices(cls): + return tuple((i.name, i.value) for i in cls) + + @classmethod + def get_risk_level(cls, value): + # map risk value + if value == 0: + return cls.NO_RISK.value + elif 1 <= value <= 2: + return cls.LOW.value + elif 3 <= value <= 4: + return cls.MEDIUM.value + elif value >= 5: + return cls.HIGH.value + else: + raise ValueError("Risk value not valid") + + +class AlertDetectionType(Enum): + """Types of possible alert detections + + * NEW_DEVICE: Login from a new user-agent used by the user + * IMP_TRAVEL: Alert if the user logs into the system from a significant distance () within a range of time that cannot be covered by conventional means of transport + * NEW_COUNTRY: The user made a login from a country where they have never logged in before + * USER_RISK_THRESHOLD: + * LOGIN_ANONYMIZER_IP: + * ATYPICAL_COUNTRY + """ + + NEW_DEVICE = "Login from new device" + IMP_TRAVEL = "Impossible Travel detected" + NEW_COUNTRY = "Login from new country" + USER_RISK_THRESHOLD = "User risk threshold alert" + LOGIN_ANONYMIZER_IP = "Login from anonymizer IP" + ATYPICAL_COUNTRY = "Login from atypical country" + + @classmethod + def choices(cls): + return tuple((i.name, i.value) for i in cls) + + +class AlertFilterType(Enum): + """Types of possible detection filter applied on alerts to be ignored + + * ISP_FILTER: exclude from the detection a list of whitelisted ISP + * IS_MOBILE_FILTER: if Config.ignore_mobile_logins flag is checked, exclude from the detection the mobile devices + * IS_VIP_FILTER: if Config.alert_is_vip_only flag is checked, only the vip users (in the Config.vip_users list) send alerts + * ALLOWED_COUNTRY_FILTER: if the country of the login is in the Config.allowed_countries list, the alert isn't sent + * IGNORED_USER_FILTER: if the user is in the Config.ignored_users list OR the user is not in the Config.enabled_users list, the alert isn't sent + * ALERT_MINIMUM_RISK_SCORE_FILTER: if the user hasn't, at least, a User.risk_score equals to the one sets in Config.alert_minimum_risk_score, + * FILTERED_ALERTS: if the alert type (AlertDetectionType) is in the Config.filtered_alerts, the alert isn't sent + """ + + ISP_FILTER = "isp_filter" + IS_MOBILE_FILTER = "is_mobile_filter" + IS_VIP_FILTER = "is_vip_filter" + ALLOWED_COUNTRY_FILTER = "allowed_country_filter" + IGNORED_USER_FILTER = "ignored_user_filter" + ALERT_MINIMUM_RISK_SCORE_FILTER = "alert_minimum_risk_score_filter" + FILTERED_ALERTS = "filtered_alerts" + + @classmethod + def choices(cls): + return tuple((i.name, i.value) for i in cls) diff --git a/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py b/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py new file mode 100644 index 0000000..e77e113 --- /dev/null +++ b/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py @@ -0,0 +1,188 @@ +# Generated by Django 5.1.4 on 2024-12-13 10:25 + +import django.contrib.postgres.fields +import impossible_travel.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "impossible_travel", + "0010_config_alert_max_days_config_distance_accepted_and_more", + ), + ] + + operations = [ + migrations.AddField( + model_name="alert", + name="filter_type", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + blank=True, + choices=[ + ("ISP_FILTER", "isp_filter"), + ("IS_MOBILE_FILTER", "is_mobile_filter"), + ("IS_VIP_FILTER", "is_vip_filter"), + ("ALLOWED_COUNTRY_FILTER", "allowed_country_filter"), + ("IGNORED_USER_FILTER", "ignored_user_filter"), + ( + "ALERT_MINIMUM_RISK_SCORE_FILTER", + "alert_minimum_risk_score_filter", + ), + ("FILTERED_ALERTS", "filtered_alerts"), + ], + max_length=50, + ), + blank=True, + default=list, + help_text="List of filters that disabled the related alert", + size=None, + ), + ), + migrations.AddField( + model_name="alert", + name="is_filtered", + field=models.BooleanField( + default=False, + help_text="Show if the alert has been filtered because of some filter (listed in the filter_type field)", + ), + ), + migrations.AddField( + model_name="config", + name="alert_is_vip_only", + field=models.BooleanField( + default=False, + help_text="Flag to send alert only related to the users in the vip_users list", + ), + ), + migrations.AddField( + model_name="config", + name="alert_minimum_risk_score", + field=models.CharField( + choices=[ + ("NO_RISK", "No risk"), + ("LOW", "Low"), + ("MEDIUM", "Medium"), + ("HIGH", "High"), + ], + default="No risk", + help_text="Select the risk_score that users should have at least to send alert", + max_length=30, + ), + ), + migrations.AddField( + model_name="config", + name="enabled_users", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=50), + blank=True, + default=impossible_travel.models.get_default_enabled_users, + help_text="List of selected users on which the detection will perform", + size=None, + ), + ), + migrations.AddField( + model_name="config", + name="filtered_alerts_types", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + blank=True, + choices=[ + ("NEW_DEVICE", "Login from new device"), + ("IMP_TRAVEL", "Impossible Travel detected"), + ("NEW_COUNTRY", "Login from new country"), + ("USER_RISK_THRESHOLD", "User risk threshold alert"), + ("LOGIN_ANONYMIZER_IP", "Login from anonymizer IP"), + ("ATYPICAL_COUNTRY", "Login from atypical country"), + ], + max_length=50, + ), + default=list, + help_text="List of alerts' types to exclude from the alerting", + size=None, + ), + ), + migrations.AddField( + model_name="config", + name="ignore_mobile_logins", + field=models.BooleanField( + default=False, + help_text="Flag to ignore mobile devices from the detection", + ), + ), + migrations.AlterField( + model_name="alert", + name="name", + field=models.CharField( + choices=[ + ("NEW_DEVICE", "Login from new device"), + ("IMP_TRAVEL", "Impossible Travel detected"), + ("NEW_COUNTRY", "Login from new country"), + ("USER_RISK_THRESHOLD", "User risk threshold alert"), + ("LOGIN_ANONYMIZER_IP", "Login from anonymizer IP"), + ("ATYPICAL_COUNTRY", "Login from atypical country"), + ], + max_length=30, + ), + ), + migrations.AlterField( + model_name="config", + name="allowed_countries", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=20), + blank=True, + default=impossible_travel.models.get_default_allowed_countries, + help_text="List of countries to exclude from the detection, because 'trusted' for the customer", + size=None, + ), + ), + migrations.AlterField( + model_name="config", + name="ignored_ips", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=50), + blank=True, + default=impossible_travel.models.get_default_ignored_ips, + help_text="List of IPs to remove from the detection", + size=None, + ), + ), + migrations.AlterField( + model_name="config", + name="ignored_users", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=50), + blank=True, + default=impossible_travel.models.get_default_ignored_users, + help_text="List of users to be ignored from the detection", + size=None, + ), + ), + migrations.AlterField( + model_name="config", + name="vip_users", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=50), + blank=True, + default=impossible_travel.models.get_default_vip_users, + help_text="List of users considered more sensitive", + size=None, + ), + ), + migrations.AlterField( + model_name="user", + name="risk_score", + field=models.CharField( + choices=[ + ("NO_RISK", "No risk"), + ("LOW", "Low"), + ("MEDIUM", "Medium"), + ("HIGH", "High"), + ], + default="No risk", + max_length=30, + ), + ), + ] diff --git a/buffalogs/impossible_travel/models.py b/buffalogs/impossible_travel/models.py index e3605c4..87a2ee9 100644 --- a/buffalogs/impossible_travel/models.py +++ b/buffalogs/impossible_travel/models.py @@ -1,23 +1,18 @@ +from django.conf import settings from django.contrib.postgres.fields import ArrayField from django.db import models +from impossible_travel.constants import AlertDetectionType, AlertFilterType, UserRiskScoreType class User(models.Model): - class riskScoreEnum(models.TextChoices): - NO_RISK = "No risk" - LOW = "Low" - MEDIUM = "Medium" - HIGH = "High" - - risk_score = models.CharField( - choices=riskScoreEnum.choices, - max_length=256, - null=False, - ) + risk_score = models.CharField(choices=UserRiskScoreType.choices(), max_length=30, null=False, default=UserRiskScoreType.NO_RISK.value) username = models.TextField(unique=True, db_index=True) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) + def get_risk_display(self): + return AlertFilterType.get_risk_level(self.risk_level).name + class Login(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) @@ -34,14 +29,9 @@ class Login(models.Model): class Alert(models.Model): - class ruleNameEnum(models.TextChoices): - NEW_DEVICE = "Login from new device" - IMP_TRAVEL = "Impossible Travel detected" - NEW_COUNTRY = "Login from new country" - name = models.CharField( - choices=ruleNameEnum.choices, - max_length=256, + choices=AlertDetectionType.choices(), + max_length=30, null=False, ) user = models.ForeignKey(User, on_delete=models.CASCADE) @@ -50,6 +40,13 @@ class ruleNameEnum(models.TextChoices): updated = models.DateTimeField(auto_now=True) description = models.TextField() is_vip = models.BooleanField(default=False) + is_filtered = models.BooleanField(default=False, help_text="Show if the alert has been filtered because of some filter (listed in the filter_type field)") + filter_type = ArrayField( + models.CharField(max_length=50, choices=AlertFilterType.choices(), blank=True), + blank=True, + default=list, + help_text="List of filters that disabled the related alert", + ) class UsersIP(models.Model): @@ -67,20 +64,72 @@ class TaskSettings(models.Model): end_date = models.DateTimeField() +def get_default_ignored_users(): + return list(settings.CERTEGO_BUFFALOGS_IGNORED_USERS) + + +def get_default_enabled_users(): + return list(settings.CERTEGO_BUFFALOGS_ENABLED_USERS) + + +def get_default_ignored_ips(): + return list(settings.CERTEGO_BUFFALOGS_IGNORED_IPS) + + +def get_default_allowed_countries(): + return list(settings.CERTEGO_BUFFALOGS_ALLOWED_COUNTRIES) + + +def get_default_vip_users(): + return list(settings.CERTEGO_BUFFALOGS_VIP_USERS) + + class Config(models.Model): created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) - ignored_users = ArrayField(models.CharField(max_length=20), blank=True, default=list) - ignored_ips = ArrayField(models.CharField(max_length=100), blank=True, default=list) - allowed_countries = ArrayField(models.CharField(max_length=20), blank=True, default=list) - vip_users = ArrayField(models.CharField(max_length=100), blank=True, default=list) + ignored_users = ArrayField( + models.CharField(max_length=50), blank=True, default=get_default_ignored_users, help_text="List of users to be ignored from the detection" + ) + enabled_users = ArrayField( + models.CharField(max_length=50), blank=True, default=get_default_enabled_users, help_text="List of selected users on which the detection will perform" + ) + ignored_ips = ArrayField(models.CharField(max_length=50), blank=True, default=get_default_ignored_ips, help_text="List of IPs to remove from the detection") + allowed_countries = ArrayField( + models.CharField(max_length=20), + blank=True, + default=get_default_allowed_countries, + help_text="List of countries to exclude from the detection, because 'trusted' for the customer", + ) + vip_users = ArrayField(models.CharField(max_length=50), blank=True, default=get_default_vip_users, help_text="List of users considered more sensitive") + alert_is_vip_only = models.BooleanField(default=False, help_text="Flag to send alert only related to the users in the vip_users list") + alert_minimum_risk_score = models.CharField( + choices=UserRiskScoreType.choices(), + max_length=30, + blank=False, + default=UserRiskScoreType.NO_RISK.value, + help_text="Select the risk_score that users should have at least to send alert", + ) + filtered_alerts_types = ArrayField( + models.CharField(max_length=50, choices=AlertDetectionType.choices(), blank=True), + default=list, + help_text="List of alerts' types to exclude from the alerting", + ) + ignore_mobile_logins = models.BooleanField(default=False, help_text="Flag to ignore mobile devices from the detection") distance_accepted = models.PositiveIntegerField( - default=100, help_text="Minimum distance (in Km) between two logins after which the impossible travel detection starts" + default=settings.CERTEGO_BUFFALOGS_DISTANCE_KM_ACCEPTED, + help_text="Minimum distance (in Km) between two logins after which the impossible travel detection starts", ) vel_accepted = models.PositiveIntegerField( - default=300, help_text="Minimum velocity (in Km/h) between two logins after which the impossible travel detection starts" + default=settings.CERTEGO_BUFFALOGS_VEL_TRAVEL_ACCEPTED, + help_text="Minimum velocity (in Km/h) between two logins after which the impossible travel detection starts", + ) + user_max_days = models.PositiveIntegerField( + default=settings.CERTEGO_BUFFALOGS_USER_MAX_DAYS, help_text="Days after which the users will be removed from the db" + ) + login_max_days = models.PositiveIntegerField( + default=settings.CERTEGO_BUFFALOGS_LOGIN_MAX_DAYS, help_text="Days after which the logins will be removed from the db" + ) + alert_max_days = models.PositiveIntegerField( + default=settings.CERTEGO_BUFFALOGS_ALERT_MAX_DAYS, help_text="Days after which the alerts will be removed from the db" ) - user_max_days = models.PositiveIntegerField(default=60, help_text="Days after which the users will be removed from the db") - login_max_days = models.PositiveIntegerField(default=30, help_text="Days after which the logins will be removed from the db") - alert_max_days = models.PositiveIntegerField(default=30, help_text="Days after which the alerts will be removed from the db") - ip_max_days = models.PositiveIntegerField(default=30, help_text="Days after which the IPs will be removed from the db") + ip_max_days = models.PositiveIntegerField(default=settings.CERTEGO_BUFFALOGS_IP_MAX_DAYS, help_text="Days after which the IPs will be removed from the db") diff --git a/buffalogs/impossible_travel/modules/impossible_travel.py b/buffalogs/impossible_travel/modules/impossible_travel.py index 290f8c2..f189577 100644 --- a/buffalogs/impossible_travel/modules/impossible_travel.py +++ b/buffalogs/impossible_travel/modules/impossible_travel.py @@ -3,6 +3,7 @@ from django.utils import timezone from geopy.distance import geodesic +from impossible_travel.constants import AlertDetectionType, UserRiskScoreType from impossible_travel.models import Alert, Config, Login, UsersIP @@ -44,7 +45,7 @@ def calc_distance(self, db_user, prev_login, last_login_user_fields): vel = distance_km / diff_timestamp_hours if vel > app_config.vel_accepted: - alert_info["alert_name"] = Alert.ruleNameEnum.IMP_TRAVEL + alert_info["alert_name"] = AlertDetectionType.IMP_TRAVEL.value alert_info[ "alert_desc" ] = f"{alert_info['alert_name']} for User: {db_user.username}, at: {last_timestamp_datetimeObj_aware}, from: {last_login_user_fields['country']}, previous country: {prev_login.country}, distance covered at {int(vel)} Km/h" diff --git a/buffalogs/impossible_travel/modules/login_from_new_country.py b/buffalogs/impossible_travel/modules/login_from_new_country.py index d66e689..7e8ef2e 100644 --- a/buffalogs/impossible_travel/modules/login_from_new_country.py +++ b/buffalogs/impossible_travel/modules/login_from_new_country.py @@ -1,5 +1,6 @@ import logging +from impossible_travel.constants import AlertDetectionType from impossible_travel.models import Alert from impossible_travel.modules import impossible_travel @@ -19,6 +20,6 @@ def check_country(self, db_user, login_field): new_country = login_field["country"] if db_user.login_set.filter(country=new_country).count() == 0: time = login_field["timestamp"] - alert_info["alert_name"] = Alert.ruleNameEnum.NEW_COUNTRY + alert_info["alert_name"] = AlertDetectionType.NEW_COUNTRY.value alert_info["alert_desc"] = f"{alert_info['alert_name']} for User: {db_user.username}, at: {time}, from: {new_country}" return alert_info diff --git a/buffalogs/impossible_travel/modules/login_from_new_device.py b/buffalogs/impossible_travel/modules/login_from_new_device.py index aa321cc..6b04ce4 100644 --- a/buffalogs/impossible_travel/modules/login_from_new_device.py +++ b/buffalogs/impossible_travel/modules/login_from_new_device.py @@ -1,5 +1,6 @@ import logging +from impossible_travel.constants import AlertDetectionType from impossible_travel.models import Alert from impossible_travel.modules import impossible_travel @@ -21,10 +22,6 @@ def check_new_device(self, db_user, login_field): alert_info = {} if db_user.login_set.filter(user_agent=login_field["agent"]).count() == 0: timestamp = login_field["timestamp"] - alert_info["alert_name"] = Alert.ruleNameEnum.NEW_DEVICE - alert_info[ - "alert_desc" - ] = f"LOGIN FROM NEW DEVICE\ - for User: {db_user.username},\ - at: {timestamp}" + alert_info["alert_name"] = AlertDetectionType.NEW_DEVICE.value + alert_info["alert_desc"] = f"LOGIN FROM NEW DEVICE for User: {db_user.username}, at: {timestamp}" return alert_info diff --git a/buffalogs/impossible_travel/tasks.py b/buffalogs/impossible_travel/tasks.py index 6e214f7..f430430 100644 --- a/buffalogs/impossible_travel/tasks.py +++ b/buffalogs/impossible_travel/tasks.py @@ -7,6 +7,7 @@ from django.db.models import Count from django.utils import timezone from elasticsearch_dsl import Search, connections +from impossible_travel.constants import UserRiskScoreType from impossible_travel.models import Alert, Config, Login, TaskSettings, User, UsersIP from impossible_travel.modules import impossible_travel, login_from_new_country, login_from_new_device @@ -37,18 +38,10 @@ def update_risk_level(): with transaction.atomic(): for u in User.objects.annotate(Count("alert")): alerts_num = u.alert__count - if alerts_num == 0: - tmp = User.riskScoreEnum.NO_RISK - elif 1 <= alerts_num <= 2: - tmp = User.riskScoreEnum.LOW - elif 3 <= alerts_num <= 4: - tmp = User.riskScoreEnum.MEDIUM - else: - tmp = User.riskScoreEnum.HIGH - if u.risk_score != tmp: - # Added log only if it's updated, not always for each High risk user - logger.info(f"{User.riskScoreEnum.HIGH} risk level for User: {u.username}, {alerts_num} detected") + tmp = UserRiskScoreType.get_risk_level(alerts_num) if u.risk_score != tmp: + # Added log only if it's updated, not always for each High risk user + logger.info(f"Upgraded risk level for User: {u.username}, {alerts_num} detected") u.risk_score = tmp u.save() @@ -64,7 +57,7 @@ def set_alert(db_user, login_alert, alert_info): :type alert_info: dict """ logger.info( - f"ALERT {alert_info['alert_name']} for User:{db_user.username} at:{login_alert['timestamp']} from {login_alert['country']} from device:{login_alert['agent']}" + f"ALERT {alert_info['alert_name']} for User: {db_user.username} at: {login_alert['timestamp']} from {login_alert['country']} from device: {login_alert['agent']}" ) alert = Alert.objects.create(user_id=db_user.id, login_raw_data=login_alert, name=alert_info["alert_name"], description=alert_info["alert_desc"]) if Config.objects.filter(vip_users__contains=[db_user.username]): @@ -221,7 +214,7 @@ def exec_process_logs(start_date, end_date): :param end_date: End datetime :type end_date: datetime """ - logger.info(f"Starting at:{start_date} Finishing at:{end_date}") + logger.info(f"Starting at: {start_date} Finishing at: {end_date}") config, op_result = Config.objects.get_or_create() connections.create_connection(hosts=settings.CERTEGO_ELASTICSEARCH, timeout=90, verify_certs=False) s = ( diff --git a/buffalogs/impossible_travel/tests/test_impossible_travel.py b/buffalogs/impossible_travel/tests/test_impossible_travel.py index c6116c2..2019f17 100644 --- a/buffalogs/impossible_travel/tests/test_impossible_travel.py +++ b/buffalogs/impossible_travel/tests/test_impossible_travel.py @@ -56,7 +56,7 @@ def test_calc_distance_alert(self): db_user = User.objects.get(username="Lorena Goldoni") prev_login = Login.objects.get(id=db_user.id) result, vel = self.imp_travel.calc_distance(db_user, prev_login, last_login_user_fields) - self.assertEqual("Impossible Travel detected", result["alert_name"].value) + self.assertEqual("Impossible Travel detected", result["alert_name"]) self.assertIn("for User: Lorena Goldoni", result["alert_desc"]) self.assertIn("from: Sudan", result["alert_desc"]) self.assertIn("previous country: United States, distance covered at 10109599 Km/h", result["alert_desc"]) diff --git a/buffalogs/impossible_travel/tests/test_login_from_new_country.py b/buffalogs/impossible_travel/tests/test_login_from_new_country.py index f9e888b..6d7fb67 100644 --- a/buffalogs/impossible_travel/tests/test_login_from_new_country.py +++ b/buffalogs/impossible_travel/tests/test_login_from_new_country.py @@ -45,4 +45,4 @@ def test_check_country_alert(self): "user_agent": "Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.9.1.8) Gecko/20100214 Ubuntu/9.10 (karmic) Firefox/3.5.8", } alert_result = self.new_country.check_country(db_user, last_login_user_fields) - self.assertEqual("Login from new country", alert_result["alert_name"].value) + self.assertEqual("Login from new country", alert_result["alert_name"]) diff --git a/buffalogs/impossible_travel/tests/test_login_from_new_device.py b/buffalogs/impossible_travel/tests/test_login_from_new_device.py index 51701eb..4ec088d 100644 --- a/buffalogs/impossible_travel/tests/test_login_from_new_device.py +++ b/buffalogs/impossible_travel/tests/test_login_from_new_device.py @@ -45,4 +45,4 @@ def test_check_new_device_alert(self): "agent": "Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.9.1.8) Gecko/20100214 Ubuntu/9.10 (karmic) Firefox/3.5.8", } alert_result = self.new_device.check_new_device(db_user, last_login_user_fields) - self.assertEqual("Login from new device", alert_result["alert_name"].value) + self.assertEqual("Login from new device", alert_result["alert_name"]) diff --git a/buffalogs/impossible_travel/tests/test_tasks.py b/buffalogs/impossible_travel/tests/test_tasks.py index e8dcbfc..4ef9560 100644 --- a/buffalogs/impossible_travel/tests/test_tasks.py +++ b/buffalogs/impossible_travel/tests/test_tasks.py @@ -6,6 +6,7 @@ from django.test import TestCase from django.utils import timezone from impossible_travel import tasks +from impossible_travel.constants import AlertDetectionType from impossible_travel.models import Alert, Config, Login, TaskSettings, User, UsersIP from impossible_travel.tests.setup import Setup @@ -72,7 +73,7 @@ def test_update_risk_level_low(self): # 1 alert --> Low risk self.assertTrue(User.objects.filter(username="Lorena Goldoni").exists()) db_user = User.objects.get(username="Lorena Goldoni") - Alert.objects.create(user=db_user, name=Alert.ruleNameEnum.IMP_TRAVEL, login_raw_data="Test", description="Test_Description") + Alert.objects.create(user=db_user, name=AlertDetectionType.IMP_TRAVEL.value, login_raw_data="Test", description="Test_Description") tasks.update_risk_level() db_user = User.objects.get(username="Lorena Goldoni") self.assertEqual("Low", db_user.risk_score) @@ -84,9 +85,9 @@ def test_update_risk_level_medium(self): db_user = User.objects.get(username="Lorena Goldoni") Alert.objects.bulk_create( [ - Alert(user=db_user, name=Alert.ruleNameEnum.IMP_TRAVEL, login_raw_data="Test1", description="Test_Description1"), - Alert(user=db_user, name=Alert.ruleNameEnum.NEW_DEVICE, login_raw_data="Test2", description="Test_Description2"), - Alert(user=db_user, name=Alert.ruleNameEnum.NEW_COUNTRY, login_raw_data="Test3", description="Test_Description3"), + Alert(user=db_user, name=AlertDetectionType.IMP_TRAVEL.value, login_raw_data="Test1", description="Test_Description1"), + Alert(user=db_user, name=AlertDetectionType.NEW_DEVICE.value, login_raw_data="Test2", description="Test_Description2"), + Alert(user=db_user, name=AlertDetectionType.NEW_COUNTRY.value, login_raw_data="Test3", description="Test_Description3"), ] ) tasks.update_risk_level() @@ -100,11 +101,11 @@ def test_update_risk_level_high(self): db_user = User.objects.get(username="Lorena Goldoni") Alert.objects.bulk_create( [ - Alert(user=db_user, name=Alert.ruleNameEnum.IMP_TRAVEL, login_raw_data="Test1", description="Test_Description1"), - Alert(user=db_user, name=Alert.ruleNameEnum.NEW_DEVICE, login_raw_data="Test2", description="Test_Description2"), - Alert(user=db_user, name=Alert.ruleNameEnum.NEW_COUNTRY, login_raw_data="Test3", description="Test_Description3"), - Alert(user=db_user, name=Alert.ruleNameEnum.NEW_COUNTRY, login_raw_data="Test4", description="Test_Description4"), - Alert(user=db_user, name=Alert.ruleNameEnum.NEW_COUNTRY, login_raw_data="Test5", description="Test_Description5"), + Alert(user=db_user, name=AlertDetectionType.IMP_TRAVEL.value, login_raw_data="Test1", description="Test_Description1"), + Alert(user=db_user, name=AlertDetectionType.NEW_DEVICE.value, login_raw_data="Test2", description="Test_Description2"), + Alert(user=db_user, name=AlertDetectionType.NEW_COUNTRY.value, login_raw_data="Test3", description="Test_Description3"), + Alert(user=db_user, name=AlertDetectionType.NEW_COUNTRY.value, login_raw_data="Test4", description="Test_Description4"), + Alert(user=db_user, name=AlertDetectionType.NEW_COUNTRY.value, login_raw_data="Test5", description="Test_Description5"), ] ) tasks.update_risk_level() @@ -117,15 +118,15 @@ def test_set_alert(self): db_login = Login.objects.get(user_agent="Mozilla/5.0 (X11;U; Linux i686; en-GB; rv:1.9.1) Gecko/20090624 Ubuntu/9.04 (jaunty) Firefox/3.5") timestamp = db_login.timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ") login_data = {"timestamp": timestamp, "latitude": "45.4758", "longitude": "9.2275", "country": db_login.country, "agent": db_login.user_agent} - name = Alert.ruleNameEnum.IMP_TRAVEL - desc = f"{name} for User: {db_user.username},\ - at: {timestamp}, from:({db_login.latitude}, {db_login.longitude})" + name = AlertDetectionType.IMP_TRAVEL.value + desc = f"{name} for User: {db_user.username}, \ + at: {timestamp}, from: ({db_login.latitude}, {db_login.longitude})" alert_info = { "alert_name": name, "alert_desc": desc, } tasks.set_alert(db_user, login_data, alert_info) - db_alert = Alert.objects.get(user=db_user, name=Alert.ruleNameEnum.IMP_TRAVEL) + db_alert = Alert.objects.get(user=db_user, name=AlertDetectionType.IMP_TRAVEL.value) self.assertIsNotNone(db_alert) self.assertEqual("Impossible Travel detected", db_alert.name) self.assertFalse(db_alert.is_vip) @@ -136,15 +137,15 @@ def test_set_alert_vip_user(self): db_login = Login.objects.filter(user=db_user).first() timestamp = db_login.timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ") login_data = {"timestamp": timestamp, "latitude": "45.4758", "longitude": "9.2275", "country": db_login.country, "agent": db_login.user_agent} - name = Alert.ruleNameEnum.IMP_TRAVEL - desc = f"{name} for User: {db_user.username},\ - at: {timestamp}, from:({db_login.latitude}, {db_login.longitude})" + name = AlertDetectionType.IMP_TRAVEL.value + desc = f"{name} for User: {db_user.username}, \ + at: {timestamp}, from: ({db_login.latitude}, {db_login.longitude})" alert_info = { "alert_name": name, "alert_desc": desc, } tasks.set_alert(db_user, login_data, alert_info) - db_alert = Alert.objects.get(user=db_user, name=Alert.ruleNameEnum.IMP_TRAVEL) + db_alert = Alert.objects.get(user=db_user, name=AlertDetectionType.IMP_TRAVEL.value) self.assertTrue(db_alert.is_vip) def test_process_logs_data_lost(self): @@ -274,9 +275,9 @@ def check_fields_alerts(self): # 5. at 2023-05-03T06:57:27.768Z alert IMP TRAVEL # 6. at 2023-05-03T07:10:23.154Z alert IMP TRAVEL self.assertEqual(6, Alert.objects.filter(user=db_user).count()) - self.assertEqual(2, Alert.objects.filter(user=db_user, name=Alert.ruleNameEnum.NEW_DEVICE).count()) - self.assertEqual(1, Alert.objects.filter(user=db_user, name=Alert.ruleNameEnum.NEW_COUNTRY).count()) - self.assertEqual(3, Alert.objects.filter(user=db_user, name=Alert.ruleNameEnum.IMP_TRAVEL).count()) + self.assertEqual(2, Alert.objects.filter(user=db_user, name=AlertDetectionType.NEW_DEVICE.value).count()) + self.assertEqual(1, Alert.objects.filter(user=db_user, name=AlertDetectionType.NEW_COUNTRY.value).count()) + self.assertEqual(3, Alert.objects.filter(user=db_user, name=AlertDetectionType.IMP_TRAVEL.value).count()) self.assertEqual(0, Alert.objects.filter(user=db_user, is_vip=True).count()) # Adding "Aisha Delgado" to vip users @@ -290,9 +291,9 @@ def check_fields_alerts(self): # 10. at 2023-05-03T07:18:38.768Z alert IMP TRAVEL # 11. at 2023-05-03T07:20:36.154Z alert IMP TRAVEL tasks.check_fields(db_user, fields2) - self.assertEqual(4, Alert.objects.filter(user=db_user, name=Alert.ruleNameEnum.NEW_DEVICE).count()) - self.assertEqual(1, Alert.objects.filter(user=db_user, name=Alert.ruleNameEnum.NEW_COUNTRY).count()) - self.assertEqual(6, Alert.objects.filter(user=db_user, name=Alert.ruleNameEnum.IMP_TRAVEL).count()) + self.assertEqual(4, Alert.objects.filter(user=db_user, name=AlertDetectionType.NEW_DEVICE.value).count()) + self.assertEqual(1, Alert.objects.filter(user=db_user, name=AlertDetectionType.NEW_COUNTRY.value).count()) + self.assertEqual(6, Alert.objects.filter(user=db_user, name=AlertDetectionType.IMP_TRAVEL.value).count()) self.assertEqual(5, Alert.objects.filter(user=db_user, is_vip=True).count()) self.assertEqual(11, Alert.objects.filter(user=db_user).count()) diff --git a/buffalogs/impossible_travel/tests/test_views.py b/buffalogs/impossible_travel/tests/test_views.py index bc09c0c..6eea8df 100644 --- a/buffalogs/impossible_travel/tests/test_views.py +++ b/buffalogs/impossible_travel/tests/test_views.py @@ -3,6 +3,7 @@ from django.test import Client from django.urls import reverse +from impossible_travel.constants import AlertDetectionType, UserRiskScoreType from impossible_travel.models import Alert, Login, User from rest_framework.test import APITestCase @@ -12,11 +13,11 @@ def setUp(self): self.client = Client() User.objects.bulk_create( [ - User(username="Lorena Goldoni", risk_score=User.riskScoreEnum.NO_RISK), - User(username="Lorygold", risk_score=User.riskScoreEnum.LOW), - User(username="Lory", risk_score=User.riskScoreEnum.LOW), - User(username="Lor", risk_score=User.riskScoreEnum.LOW), - User(username="Loryg", risk_score=User.riskScoreEnum.MEDIUM), + User(username="Lorena Goldoni", risk_score=UserRiskScoreType.NO_RISK.value), + User(username="Lorygold", risk_score=UserRiskScoreType.LOW.value), + User(username="Lory", risk_score=UserRiskScoreType.LOW.value), + User(username="Lor", risk_score=UserRiskScoreType.LOW.value), + User(username="Loryg", risk_score=UserRiskScoreType.MEDIUM.value), ] ) db_user = User.objects.get(username="Lorena Goldoni") @@ -72,7 +73,7 @@ def setUp(self): [ Alert( user=db_user, - name=Alert.ruleNameEnum.NEW_DEVICE, + name=AlertDetectionType.NEW_DEVICE.value, login_raw_data={ "id": "ht9DEIgBnkLiMp6r-SG-", "ip": "203.0.113.24", @@ -87,7 +88,7 @@ def setUp(self): ), Alert( user=db_user, - name=Alert.ruleNameEnum.IMP_TRAVEL, + name=AlertDetectionType.IMP_TRAVEL.value, login_raw_data={ "id": "vfraw14gw", "ip": "1.2.3.4", @@ -102,7 +103,7 @@ def setUp(self): ), Alert( user=db_user, - name=Alert.ruleNameEnum.IMP_TRAVEL, + name=AlertDetectionType.IMP_TRAVEL.value, login_raw_data={ "id": "vfraw14gw", "ip": "1.2.3.4", @@ -117,7 +118,7 @@ def setUp(self): ), Alert( user=db_user, - name=Alert.ruleNameEnum.IMP_TRAVEL, + name=AlertDetectionType.IMP_TRAVEL.value, login_raw_data={ "id": "vfraw14gw", "ip": "1.2.3.4", @@ -132,7 +133,7 @@ def setUp(self): ), Alert( user=db_user, - name=Alert.ruleNameEnum.NEW_DEVICE, + name=AlertDetectionType.NEW_DEVICE.value, login_raw_data={ "id": "ht9DEIgBnkLiMp6r-SG-", "ip": "203.0.113.24", @@ -147,7 +148,7 @@ def setUp(self): ), Alert( user=db_user, - name=Alert.ruleNameEnum.NEW_DEVICE, + name=AlertDetectionType.NEW_DEVICE.value, login_raw_data={ "id": "ht9DEIgBnkLiMp6r-SG-", "ip": "203.0.113.24", @@ -223,7 +224,7 @@ def test_alerts_api(self): ] response = self.client.get(f"{reverse('alerts_api')}?start={start.strftime('%Y-%m-%dT%H:%M:%SZ')}&end={end.strftime('%Y-%m-%dT%H:%M:%SZ')}") self.assertEqual(response.status_code, 200) - self.assertListEqual(list_expected_result, json.loads(response.content)) + self.assertCountEqual(list_expected_result, json.loads(response.content)) def test_risk_score_api(self): end = datetime.now() + timedelta(seconds=1) From 344f5954f00ccf48577a915405511d8a23abc279 Mon Sep 17 00:00:00 2001 From: Lorena Goldoni <33703137+Lorygold@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:00:28 +0100 Subject: [PATCH 03/15] Alerts fields utilities (#105) * Added method to get the alert name label, for tagging * Added ipython for develop * Added new fields in login_raw_data for more info about previous login * Updated CHANGELOG.md --- CHANGELOG.md | 1 + buffalogs/impossible_travel/constants.py | 7 +++++++ buffalogs/impossible_travel/tasks.py | 10 +++++++--- buffalogs/requirements_dev.txt | 1 + 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb753b8..613410d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### 1.3.0 #### Feature * Added configuration panel in order to set custom preferences +* Added more fields in the Alert.login_raw_data dict in order to have more info about previous location for imp_travel detection ### Changes * Set default settings values in the *settings.certego.py* file * Moved Enums into *costants.py* file diff --git a/buffalogs/impossible_travel/constants.py b/buffalogs/impossible_travel/constants.py index 2fb9d7a..ba3e938 100644 --- a/buffalogs/impossible_travel/constants.py +++ b/buffalogs/impossible_travel/constants.py @@ -56,6 +56,13 @@ class AlertDetectionType(Enum): def choices(cls): return tuple((i.name, i.value) for i in cls) + @classmethod + def get_label_from_value(cls, value): + for item in cls: + if item.value == value: + return item.name + return None + class AlertFilterType(Enum): """Types of possible detection filter applied on alerts to be ignored diff --git a/buffalogs/impossible_travel/tasks.py b/buffalogs/impossible_travel/tasks.py index f430430..c424e18 100644 --- a/buffalogs/impossible_travel/tasks.py +++ b/buffalogs/impossible_travel/tasks.py @@ -90,12 +90,16 @@ def check_fields(db_user, fields): set_alert(db_user, login_alert=login, alert_info=country_alert) if not db_user.usersip_set.filter(ip=login["ip"]).exists(): + last_user_login = db_user.login_set.latest("timestamp") logger.info(f"Calculating impossible travel: {login['id']}") - travel_alert, travel_vel = imp_travel.calc_distance(db_user, prev_login=db_user.login_set.latest("timestamp"), last_login_user_fields=login) + travel_alert, travel_vel = imp_travel.calc_distance(db_user, prev_login=last_user_login, last_login_user_fields=login) if travel_alert: new_alert = set_alert(db_user, login_alert=login, alert_info=travel_alert) - new_alert.login_raw_data["buffalogs.start_country"] = db_user.login_set.latest("timestamp").country - new_alert.login_raw_data["buffalogs.avg_speed"] = travel_vel + new_alert.login_raw_data["buffalogs"] = {} + new_alert.login_raw_data["buffalogs"]["start_country"] = last_user_login.country + new_alert.login_raw_data["buffalogs"]["avg_speed"] = travel_vel + new_alert.login_raw_data["buffalogs"]["start_lat"] = last_user_login.latitude + new_alert.login_raw_data["buffalogs"]["start_lon"] = last_user_login.longitude new_alert.save() # Add the new ip address from which the login comes to the db imp_travel.add_new_user_ip(db_user, login["ip"]) diff --git a/buffalogs/requirements_dev.txt b/buffalogs/requirements_dev.txt index eb8ace9..4c24ce1 100644 --- a/buffalogs/requirements_dev.txt +++ b/buffalogs/requirements_dev.txt @@ -2,4 +2,5 @@ pre-commit==2.21.0 mypy-extensions>=0.4.3 flake8>=3.8.4 flake8-django>=1.1.5 +ipython>=8.30.0 pipdeptree>=2.16.0 \ No newline at end of file From 70a348f3aa871f65f90f2631536b217da5af77b3 Mon Sep 17 00:00:00 2001 From: Lorygold Date: Mon, 16 Dec 2024 17:11:02 +0100 Subject: [PATCH 04/15] Version 1.3.0 --- django-buffalogs/buffalogs.egg-info/PKG-INFO | 36 ++++++++++++++++--- .../buffalogs.egg-info/SOURCES.txt | 11 +++--- .../buffalogs.egg-info/requires.txt | 16 ++++----- django-buffalogs/setup.cfg | 2 +- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/django-buffalogs/buffalogs.egg-info/PKG-INFO b/django-buffalogs/buffalogs.egg-info/PKG-INFO index cc2a6fe..3af920d 100644 --- a/django-buffalogs/buffalogs.egg-info/PKG-INFO +++ b/django-buffalogs/buffalogs.egg-info/PKG-INFO @@ -1,11 +1,9 @@ Metadata-Version: 2.1 Name: buffalogs -Version: 1.2.11 +Version: 1.3.0 Summary: A Django app to detect anomaly logins. -Home-page: UNKNOWN Author: Lorena Goldoni License: Apache-2.0 -Platform: UNKNOWN Classifier: Framework :: Django Classifier: License :: OSI Approved :: Apache-2.0 Licence Classifier: Operating System :: OS Independent @@ -13,6 +11,36 @@ Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Software Development Requires-Python: >=3.8 License-File: LICENSE.txt +Requires-Dist: celery>=5.2.7 +Requires-Dist: certifi>=2022.9.24 +Requires-Dist: cfgv>=3.3.1 +Requires-Dist: distlib>=0.3.6 +Requires-Dist: Django>=4.1.4 +Requires-Dist: djangorestframework>=3.14.0 +Requires-Dist: djangorestframework-simplejwt>=5.3.0 +Requires-Dist: django-cors-headers>=4.3.0 +Requires-Dist: django-environ>=0.9.0 +Requires-Dist: djangorestframework>=3.14.0 +Requires-Dist: elasticsearch>=7.17.7 +Requires-Dist: elasticsearch-dsl>=7.4.0 +Requires-Dist: filelock>=3.9.0 +Requires-Dist: geographiclib>=2.0 +Requires-Dist: geopy>=2.3.0 +Requires-Dist: kombu>=5.2.4 +Requires-Dist: nodeenv>=1.7.0 +Requires-Dist: pathspec>=0.10.3 +Requires-Dist: prompt-toolkit>=3.0.33 +Requires-Dist: psycopg>=3.1.12 +Requires-Dist: psycopg-binary>=3.1.12 +Requires-Dist: pygal>=3.0.0 +Requires-Dist: pygal-maps-world>=1.0.2 +Requires-Dist: python-dateutil>=2.8.2 +Requires-Dist: python-dotenv>=0.21.0 +Requires-Dist: PyYAML>=6.0 +Requires-Dist: urllib3>=1.26.12 +Requires-Dist: uWSGI>=2.0.21 +Requires-Dist: virtualenv>=20.17.1 +Requires-Dist: wcwidth>=0.2.5 ========= BuffaLogs @@ -56,5 +84,3 @@ After that, the new package contained in the `django-buffalogs/dist` folder can In the other projects, install the app with ``python -m pip install buffalogs-.tar.gz`` command. If you want to uninstall the application, run ``python -m pip uninstall buffalogs``. - - diff --git a/django-buffalogs/buffalogs.egg-info/SOURCES.txt b/django-buffalogs/buffalogs.egg-info/SOURCES.txt index 44a332a..a7853dc 100644 --- a/django-buffalogs/buffalogs.egg-info/SOURCES.txt +++ b/django-buffalogs/buffalogs.egg-info/SOURCES.txt @@ -13,15 +13,13 @@ docs/static/cover_buffalogs.png impossible_travel/__init__.py impossible_travel/admin.py impossible_travel/apps.py +impossible_travel/constants.py impossible_travel/models.py impossible_travel/tasks.py impossible_travel/views.py impossible_travel/management/commands/clear_models.py impossible_travel/management/commands/impossible_travel.py impossible_travel/management/commands/setup_config.py -impossible_travel/management/commands/__pycache__/clear_models.cpython-310.pyc -impossible_travel/management/commands/__pycache__/impossible_travel.cpython-310.pyc -impossible_travel/management/commands/__pycache__/setup_config.cpython-310.pyc impossible_travel/migrations/0001_initial.py impossible_travel/migrations/0002_alert_updated.py impossible_travel/migrations/0003_alter_alert_updated.py @@ -32,10 +30,11 @@ impossible_travel/migrations/0007_login_event_id_login_ip.py impossible_travel/migrations/0008_usersip.py impossible_travel/migrations/0009_config_ignored_ips_config_ignored_users_and_more.py impossible_travel/migrations/0010_config_alert_max_days_config_distance_accepted_and_more.py +impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py impossible_travel/migrations/__init__.py impossible_travel/modules/impossible_travel.py impossible_travel/modules/login_from_new_country.py impossible_travel/modules/login_from_new_device.py -impossible_travel/modules/__pycache__/impossible_travel.cpython-310.pyc -impossible_travel/modules/__pycache__/login_from_new_country.cpython-310.pyc -impossible_travel/modules/__pycache__/login_from_new_device.cpython-310.pyc \ No newline at end of file +impossible_travel/modules/__pycache__/impossible_travel.cpython-312.pyc +impossible_travel/modules/__pycache__/login_from_new_country.cpython-312.pyc +impossible_travel/modules/__pycache__/login_from_new_device.cpython-312.pyc \ No newline at end of file diff --git a/django-buffalogs/buffalogs.egg-info/requires.txt b/django-buffalogs/buffalogs.egg-info/requires.txt index efc151f..4fbf065 100644 --- a/django-buffalogs/buffalogs.egg-info/requires.txt +++ b/django-buffalogs/buffalogs.egg-info/requires.txt @@ -1,16 +1,15 @@ -Django>=4.1.4 -PyYAML>=6.0 celery>=5.2.7 certifi>=2022.9.24 cfgv>=3.3.1 distlib>=0.3.6 +Django>=4.1.4 +djangorestframework>=3.14.0 +djangorestframework-simplejwt>=5.3.0 django-cors-headers>=4.3.0 django-environ>=0.9.0 -djangorestframework-simplejwt>=5.3.0 djangorestframework>=3.14.0 -djangorestframework>=3.14.0 -elasticsearch-dsl>=7.4.0 elasticsearch>=7.17.7 +elasticsearch-dsl>=7.4.0 filelock>=3.9.0 geographiclib>=2.0 geopy>=2.3.0 @@ -18,13 +17,14 @@ kombu>=5.2.4 nodeenv>=1.7.0 pathspec>=0.10.3 prompt-toolkit>=3.0.33 -psycopg-binary>=3.1.12 psycopg>=3.1.12 -pygal-maps-world>=1.0.2 +psycopg-binary>=3.1.12 pygal>=3.0.0 +pygal-maps-world>=1.0.2 python-dateutil>=2.8.2 python-dotenv>=0.21.0 -uWSGI>=2.0.21 +PyYAML>=6.0 urllib3>=1.26.12 +uWSGI>=2.0.21 virtualenv>=20.17.1 wcwidth>=0.2.5 diff --git a/django-buffalogs/setup.cfg b/django-buffalogs/setup.cfg index 0ec3dff..a4e24b7 100644 --- a/django-buffalogs/setup.cfg +++ b/django-buffalogs/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = buffalogs -version = 1.2.11 +version = 1.3.0 description = A Django app to detect anomaly logins. long_description = file: README.rst author = Lorena Goldoni From 292304ee5246d0b0dedaf89e3c954cf2402ba292 Mon Sep 17 00:00:00 2001 From: Lorena Goldoni <33703137+Lorygold@users.noreply.github.com> Date: Thu, 19 Dec 2024 20:22:47 +0100 Subject: [PATCH 05/15] Force one config object (#107) * Forced only 1 Config object presence * Set always Config.id=1 * Updated CHANGELOG.md --- CHANGELOG.md | 3 +++ buffalogs/impossible_travel/models.py | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 613410d..d2841d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ ## 1.3.x +### 1.3.1 +### Changes +* Forced the existence of only 1 Config object with id=1 ### 1.3.0 #### Feature * Added configuration panel in order to set custom preferences diff --git a/buffalogs/impossible_travel/models.py b/buffalogs/impossible_travel/models.py index 87a2ee9..a04b290 100644 --- a/buffalogs/impossible_travel/models.py +++ b/buffalogs/impossible_travel/models.py @@ -1,5 +1,6 @@ from django.conf import settings from django.contrib.postgres.fields import ArrayField +from django.core.exceptions import ValidationError from django.db import models from impossible_travel.constants import AlertDetectionType, AlertFilterType, UserRiskScoreType @@ -133,3 +134,14 @@ class Config(models.Model): default=settings.CERTEGO_BUFFALOGS_ALERT_MAX_DAYS, help_text="Days after which the alerts will be removed from the db" ) ip_max_days = models.PositiveIntegerField(default=settings.CERTEGO_BUFFALOGS_IP_MAX_DAYS, help_text="Days after which the IPs will be removed from the db") + + def clean(self): + if not self.pk and Config.objects.exists(): + raise ValidationError("A Config object already exist - it is possible just to modify it, not to create a new one") + else: + # Config.id=1 always + self.pk = 1 + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) From 0a84768e358ac37b614987d69b433272920a13a5 Mon Sep 17 00:00:00 2001 From: Lorena Goldoni <33703137+Lorygold@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:34:57 +0100 Subject: [PATCH 06/15] Fix alert name representation (#108) * Fixed alert.name representation enums * Cleaned enum * Set short_name to choose * Added forms for multiple choices array * fix * Added style in order to show the choices selected in django-admin multiple choices field Config.filtered_alerts_types * Added forms to handle multiple choice fields * Added constraints for multiple choices fields check * Fixed risk_score admin display * Added user.risk_Score choice constraint * Added timestamp representations for seconds * Fixed enums * Added test migration in order to convert data in Alert with the new Alert.name "shorter version" * Fix * Included again E231 flake8 rule, but removed in some code lines with: # noqa: E231 * Fixes * Added config.ignored_ISPs list field * Fixed Rabbit url * Added null=True for Config list fields * Completed test --- .github/configurations/python_linters/.flake8 | 4 - CHANGELOG.md | 2 + buffalogs/buffalogs/settings/certego.py | 5 +- buffalogs/impossible_travel/admin.py | 38 ++- buffalogs/impossible_travel/constants.py | 89 +++--- buffalogs/impossible_travel/forms.py | 78 ++++++ ..._filter_type_alert_is_filtered_and_more.py | 217 +++++++++++++-- ...alid_alert_filter_type_choices_and_more.py | 259 ++++++++++++++++++ buffalogs/impossible_travel/models.py | 86 ++++-- .../modules/impossible_travel.py | 2 +- .../modules/login_from_new_country.py | 2 +- .../modules/login_from_new_device.py | 2 +- .../static/css/custom_admin.css | 4 + .../tests/test_impossible_travel.py | 9 +- .../tests/test_login_from_new_country.py | 3 +- .../tests/test_login_from_new_device.py | 3 +- .../impossible_travel/tests/test_tasks.py | 87 ++++-- .../impossible_travel/tests/test_views.py | 26 +- 18 files changed, 774 insertions(+), 142 deletions(-) create mode 100644 buffalogs/impossible_travel/forms.py create mode 100644 buffalogs/impossible_travel/migrations/0012_remove_alert_valid_alert_filter_type_choices_and_more.py create mode 100644 buffalogs/impossible_travel/static/css/custom_admin.css diff --git a/.github/configurations/python_linters/.flake8 b/.github/configurations/python_linters/.flake8 index 3707569..4018556 100644 --- a/.github/configurations/python_linters/.flake8 +++ b/.github/configurations/python_linters/.flake8 @@ -17,7 +17,3 @@ ignore = exclude = */migrations/*, Dockerfile - -per-file-ignores = - # imported but unused - certego.py: E231 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d2841d8..3f3cacf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### 1.3.1 ### Changes * Forced the existence of only 1 Config object with id=1 +#### Bugfix +* Fixed alert.name representation enums ### 1.3.0 #### Feature * Added configuration panel in order to set custom preferences diff --git a/buffalogs/buffalogs/settings/certego.py b/buffalogs/buffalogs/settings/certego.py index ec4919c..8ee4f12 100644 --- a/buffalogs/buffalogs/settings/certego.py +++ b/buffalogs/buffalogs/settings/certego.py @@ -19,6 +19,7 @@ CERTEGO_BUFFALOGS_ENABLED_USERS = [] CERTEGO_BUFFALOGS_ALLOWED_COUNTRIES = [] CERTEGO_BUFFALOGS_IGNORED_IPS = ["127.0.0.1"] +CERTEGO_BUFFALOGS_IGNORED_ISPS = [] CERTEGO_BUFFALOGS_VIP_USERS = [] CERTEGO_BUFFALOGS_DISTANCE_KM_ACCEPTED = 100 CERTEGO_BUFFALOGS_VEL_TRAVEL_ACCEPTED = 300 @@ -35,7 +36,7 @@ CERTEGO_BUFFALOGS_STATIC_ROOT = "/var/www/static/" CERTEGO_BUFFALOGS_LOG_PATH = "/var/log" CERTEGO_BUFFALOGS_RABBITMQ_HOST = "rabbitmq" - CERTEGO_BUFFALOGS_RABBITMQ_URI = f"amqp://guest:guest@{CERTEGO_BUFFALOGS_RABBITMQ_HOST}/" + CERTEGO_BUFFALOGS_RABBITMQ_URI = f"amqp://guest:guest@{CERTEGO_BUFFALOGS_RABBITMQ_HOST}/" # noqa: E231 elif CERTEGO_BUFFALOGS_ENVIRONMENT == ENVIRONMENT_DEBUG: CERTEGO_ELASTICSEARCH = os.environ.get("CERTEGO_ELASTICSEARCH", "http://localhost:9200/") @@ -44,7 +45,7 @@ CERTEGO_BUFFALOGS_STATIC_ROOT = "impossible_travel/static/" CERTEGO_BUFFALOGS_LOG_PATH = "../logs" CERTEGO_BUFFALOGS_RABBITMQ_HOST = "localhost" - CERTEGO_BUFFALOGS_RABBITMQ_URI = f"amqp://guest:guest@{CERTEGO_BUFFALOGS_RABBITMQ_HOST}//" + CERTEGO_BUFFALOGS_RABBITMQ_URI = f"amqp://guest:guest@{CERTEGO_BUFFALOGS_RABBITMQ_HOST}/" # noqa: E231 else: raise ValueError(f"Environment not supported: {CERTEGO_BUFFALOGS_ENVIRONMENT}") diff --git a/buffalogs/impossible_travel/admin.py b/buffalogs/impossible_travel/admin.py index 0a7b20e..816298d 100644 --- a/buffalogs/impossible_travel/admin.py +++ b/buffalogs/impossible_travel/admin.py @@ -1,33 +1,62 @@ from django.contrib import admin +from django.utils import timezone +from .forms import AlertAdminForm, ConfigAdminForm, UserAdminForm from .models import Alert, Config, Login, TaskSettings, User, UsersIP @admin.register(Login) class LoginAdmin(admin.ModelAdmin): - list_display = ("id", "created", "updated", "get_username", "timestamp", "latitude", "longitude", "country", "user_agent", "index", "ip", "event_id") + list_display = ( + "id", + "created", + "updated", + "get_username", + "timestamp_display", + "latitude", + "longitude", + "country", + "user_agent", + "index", + "ip", + "event_id", + ) search_fields = ("id", "user__username", "user_agent", "index", "event_id", "ip") @admin.display(description="username") def get_username(self, obj): return obj.user.username + def timestamp_display(self, obj): + # Usa strftime per personalizzare il formato + return obj.timestamp.astimezone(timezone.get_current_timezone()).strftime("%b %d, %Y, %I:%M:%S %p %Z") + @admin.register(User) class UserAdmin(admin.ModelAdmin): - list_display = ("id", "username", "created", "updated", "risk_score") + form = UserAdminForm + list_display = ("id", "username", "created", "updated", "get_risk_score_value") search_fields = ("id", "username", "risk_score") + @admin.display(description="risk_score") + def get_risk_score_value(self, obj): + return obj.risk_score + @admin.register(Alert) class AlertAdmin(admin.ModelAdmin): - list_display = ("id", "created", "updated", "get_username", "name", "description", "login_raw_data", "is_vip") + form = AlertAdminForm + list_display = ("id", "created", "updated", "get_username", "get_alert_value", "description", "login_raw_data", "is_vip") search_fields = ("user__username", "name", "is_vip") @admin.display(description="username") def get_username(self, obj): return obj.user.username + @admin.display(description="name") + def get_alert_value(self, obj): + return obj.name + @admin.register(TaskSettings) class TaskSettingsAdmin(admin.ModelAdmin): @@ -37,7 +66,8 @@ class TaskSettingsAdmin(admin.ModelAdmin): @admin.register(Config) class ConfigsAdmin(admin.ModelAdmin): - list_display = ("created", "updated", "ignored_users", "ignored_ips", "allowed_countries", "vip_users") + form = ConfigAdminForm + list_display = ("created", "updated", "ignored_users", "ignored_ips", "ignored_ISPs", "allowed_countries", "vip_users") search_fields = ("allowed_countries", "vip_users") diff --git a/buffalogs/impossible_travel/constants.py b/buffalogs/impossible_travel/constants.py index ba3e938..e160e17 100644 --- a/buffalogs/impossible_travel/constants.py +++ b/buffalogs/impossible_travel/constants.py @@ -1,7 +1,10 @@ from enum import Enum +from django.db import models +from django.utils.translation import gettext_lazy as _ -class UserRiskScoreType(Enum): + +class UserRiskScoreType(models.TextChoices): """Possible types of user risk scores, based on number of alerts that they have triggered * No risk: the user has triggered 0 alerts @@ -10,14 +13,10 @@ class UserRiskScoreType(Enum): * High: the user has triggered more than 4 alerts """ - NO_RISK = "No risk" - LOW = "Low" - MEDIUM = "Medium" - HIGH = "High" - - @classmethod - def choices(cls): - return tuple((i.name, i.value) for i in cls) + NO_RISK = "No risk", _("User has no risk") + LOW = "Low", _("User has a low risk") + MEDIUM = "Medium", _("User has a medium risk") + HIGH = "High", _("User has a high risk") @classmethod def get_risk_level(cls, value): @@ -34,27 +33,23 @@ def get_risk_level(cls, value): raise ValueError("Risk value not valid") -class AlertDetectionType(Enum): - """Types of possible alert detections +class AlertDetectionType(models.TextChoices): + """Types of possible alert detections in the format (name=value,label) * NEW_DEVICE: Login from a new user-agent used by the user * IMP_TRAVEL: Alert if the user logs into the system from a significant distance () within a range of time that cannot be covered by conventional means of transport * NEW_COUNTRY: The user made a login from a country where they have never logged in before - * USER_RISK_THRESHOLD: - * LOGIN_ANONYMIZER_IP: - * ATYPICAL_COUNTRY + * USER_RISK_THRESHOLD: Alert if the user.risk_score value is equal or higher than the Config.alert_minimum_risk_score + * LOGIN_ANONYMIZER_IP: Alert if the login has been made from an anonymizer IP + * ATYPICAL_COUNTRY: Alert if the login has been made from a country not visited recently """ - NEW_DEVICE = "Login from new device" - IMP_TRAVEL = "Impossible Travel detected" - NEW_COUNTRY = "Login from new country" - USER_RISK_THRESHOLD = "User risk threshold alert" - LOGIN_ANONYMIZER_IP = "Login from anonymizer IP" - ATYPICAL_COUNTRY = "Login from atypical country" - - @classmethod - def choices(cls): - return tuple((i.name, i.value) for i in cls) + NEW_DEVICE = "New Device", _("Login from new device") + IMP_TRAVEL = "Imp Travel", _("Impossible Travel detected") + NEW_COUNTRY = "New Country", _("Login from new country") + USER_RISK_THRESHOLD = "User Risk Threshold", _("User risk higher than threshold") + LOGIN_ANONYMIZER_IP = "Login Anonymizer Ip", _("Login from an anonymizer IP") + ATYPICAL_COUNTRY = "Atypical Country", _("Login from a country not visited recently") @classmethod def get_label_from_value(cls, value): @@ -64,26 +59,34 @@ def get_label_from_value(cls, value): return None -class AlertFilterType(Enum): +class AlertFilterType(models.TextChoices): """Types of possible detection filter applied on alerts to be ignored - * ISP_FILTER: exclude from the detection a list of whitelisted ISP - * IS_MOBILE_FILTER: if Config.ignore_mobile_logins flag is checked, exclude from the detection the mobile devices - * IS_VIP_FILTER: if Config.alert_is_vip_only flag is checked, only the vip users (in the Config.vip_users list) send alerts - * ALLOWED_COUNTRY_FILTER: if the country of the login is in the Config.allowed_countries list, the alert isn't sent - * IGNORED_USER_FILTER: if the user is in the Config.ignored_users list OR the user is not in the Config.enabled_users list, the alert isn't sent - * ALERT_MINIMUM_RISK_SCORE_FILTER: if the user hasn't, at least, a User.risk_score equals to the one sets in Config.alert_minimum_risk_score, - * FILTERED_ALERTS: if the alert type (AlertDetectionType) is in the Config.filtered_alerts, the alert isn't sent + * IGNORED_USER_FILTER: Alert filtered because the user is ignored - the user is in the Config.ignored_users list or Config.enabled_users list is populated + * IGNORED_IP_FILTER: Alert filtered because the IP is ignored - the ip is in the Config.ignored_ips list + * ALLOWED_COUNTRY_FILTER: Alert filtered because the country is whitelisted - the country is in the Config.allowed_countries list + * IS_VIP_FILTER: Alert filtered because the user is not vip - Config.alert_is_vip_only is True and the usre is not in the Config.vip_users list + * ALERT_MINIMUM_RISK_SCORE_FILTER: Alert filtered because the User.risk_score is lower than the threshold set in Config.alert_minimum_risk_score + * FILTERED_ALERTS: Alert filtered because this detection type is excluded - the Alert.name detection type is in the Config.filtered_alerts_types list + * IS_MOBILE_FILTER: Alert filtered because the login is from a mobile device - Config.ignore_mobile_logins is True + * IGNORED_ISP_FILTER: Alert filtered because the ISP is whitelisted - The ISP is in the Config.ignored_ISPs list """ - ISP_FILTER = "isp_filter" - IS_MOBILE_FILTER = "is_mobile_filter" - IS_VIP_FILTER = "is_vip_filter" - ALLOWED_COUNTRY_FILTER = "allowed_country_filter" - IGNORED_USER_FILTER = "ignored_user_filter" - ALERT_MINIMUM_RISK_SCORE_FILTER = "alert_minimum_risk_score_filter" - FILTERED_ALERTS = "filtered_alerts" - - @classmethod - def choices(cls): - return tuple((i.name, i.value) for i in cls) + IGNORED_USER_FILTER = "ignored_users filter", _( + "Alert filtered because the user is ignored - the user is in the Config.ignored_users list or Config.enabled_users list is populated" + ) + IGNORED_IP_FILTER = "ignored_ips filter", _("Alert filtered because the IP is ignored - the ip is in the Config.ignored_ips list") + ALLOWED_COUNTRY_FILTER = "allowed_countries filter", _( + "Alert filtered because the country is whitelisted - the country is in the Config.allowed_countries list" + ) + IS_VIP_FILTER = "is_vip_filter", _( + "Alert filtered because the user is not vip - Config.alert_is_vip_only is True and the usre is not in the Config.vip_users list" + ) + ALERT_MINIMUM_RISK_SCORE_FILTER = "alert_minimum_risk_score filter", _( + "Alert filtered because the User.risk_score is lower than the threshold set in Config.alert_minimum_risk_score" + ) + FILTERED_ALERTS = "filtered_alerts_types filter", _( + "Alert filtered because this detection type is excluded - the Alert.name detection type is in the Config.filtered_alerts_types list" + ) + IS_MOBILE_FILTER = "ignore_mobile_logins filter", _("Alert filtered because the login is from a mobile device - Config.ignore_mobile_logins is True") + IGNORED_ISP_FILTER = "ignored_ISPs filter", _("Alert filtered because the ISP is whitelisted - The ISP is in the Config.ignored_ISPs list") diff --git a/buffalogs/impossible_travel/forms.py b/buffalogs/impossible_travel/forms.py new file mode 100644 index 0000000..c76607c --- /dev/null +++ b/buffalogs/impossible_travel/forms.py @@ -0,0 +1,78 @@ +from django import forms +from django.contrib.postgres.forms import SimpleArrayField + +from .constants import AlertDetectionType, AlertFilterType, UserRiskScoreType +from .models import Alert, Config, TaskSettings, User, UsersIP + + +class MultiChoiceArrayWidget(forms.SelectMultiple): + """Widget for user-friendly interface for ArrayField with multiple choices""" + + def __init__(self, choices, *args, **kwargs): + super().__init__(*args, **kwargs) + self.choices = choices + + def render(self, name, value, attrs=None, renderer=None): + if value is None: + value = [] + elif not isinstance(value, list): + value = [value] + return super().render(name, value, attrs, renderer) + + +class MultiChoiceArrayField(SimpleArrayField): + """Personalized field for ArrayField that supports multiple choices""" + + def __init__(self, base_field, choices, *args, **kwargs): + self.widget = MultiChoiceArrayWidget(choices=choices) + super().__init__(base_field, *args, **kwargs) + + def prepare_value(self, value): + if value is None: + return [] + return value + + +class ShortLabelChoiceField(forms.ChoiceField): + """ChoiceField personalized in order to show the short_value as label on DjangoValue""" + + def __init__(self, *args, **kwargs): + choices = kwargs.pop("choices", []) + formatted_choices = [(value, value) for value, _ in choices] + super().__init__(*args, choices=formatted_choices, **kwargs) + + +class UserAdminForm(forms.ModelForm): + risk_score = ShortLabelChoiceField(choices=UserRiskScoreType.choices) + + class Meta: + model = User + fields = "__all__" + + +class AlertAdminForm(forms.ModelForm): + name = ShortLabelChoiceField(choices=AlertDetectionType.choices) + filter_type = ShortLabelChoiceField(choices=AlertFilterType.choices) + + class Meta: + model = Alert + fields = "__all__" + + +class ConfigAdminForm(forms.ModelForm): + filtered_alerts_types = MultiChoiceArrayField( + base_field=forms.CharField(), + choices=AlertDetectionType.choices, + required=False, + help_text="Hold down “Control”, or “Command” on a Mac, to select more than one.", + ) + alert_minimum_risk_score = ShortLabelChoiceField(choices=UserRiskScoreType.choices) + + class Meta: + model = Config + fields = "__all__" + + class Media: + css = { + "all": ("css/custom_admin.css",), + } diff --git a/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py b/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py index e77e113..29dd2eb 100644 --- a/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py +++ b/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py @@ -1,8 +1,27 @@ -# Generated by Django 5.1.4 on 2024-12-13 10:25 +# Generated by Django 5.1.4 on 2024-12-22 17:54 import django.contrib.postgres.fields +import django.utils.timezone import impossible_travel.models +from impossible_travel.constants import AlertDetectionType from django.db import migrations, models +import logging + +logger = logging.getLogger(__name__) + + +def update_alert_name(apps, schema_editor): + Alert = apps.get_model("impossible_travel", "Alert") + for alert in Alert.objects.all(): + if alert.name == "Impossible Travel detected": + alert.name = AlertDetectionType.IMP_TRAVEL + elif alert.name == "Login from new country": + alert.name = AlertDetectionType.NEW_COUNTRY + elif alert.name == "Login from new device": + alert.name = AlertDetectionType.NEW_DEVICE + else: + logger.error(f"Impossible to update the alert.name of the alert with id {alert.id}") + alert.save() class Migration(migrations.Migration): @@ -22,16 +41,28 @@ class Migration(migrations.Migration): base_field=models.CharField( blank=True, choices=[ - ("ISP_FILTER", "isp_filter"), - ("IS_MOBILE_FILTER", "is_mobile_filter"), - ("IS_VIP_FILTER", "is_vip_filter"), - ("ALLOWED_COUNTRY_FILTER", "allowed_country_filter"), - ("IGNORED_USER_FILTER", "ignored_user_filter"), + ("isp_filter", "Alert filtered because the ISP is whitelisted"), + ( + "is_mobile_filter", + "Alert filtered because login from a mobile device", + ), + ("is_vip_filter", "Alert filtered because the user is not vip"), + ( + "allowed_country_filter", + "Alert filtered because the country is whitelisted", + ), + ( + "ignored_user_filter", + "Alert filtered because the user is ignored", + ), ( - "ALERT_MINIMUM_RISK_SCORE_FILTER", "alert_minimum_risk_score_filter", + "Alert filtered because the risk_score is lower than the threshold", + ), + ( + "filtered_alerts", + "Alert filtered because this detection type is excluded", ), - ("FILTERED_ALERTS", "filtered_alerts"), ], max_length=50, ), @@ -62,10 +93,10 @@ class Migration(migrations.Migration): name="alert_minimum_risk_score", field=models.CharField( choices=[ - ("NO_RISK", "No risk"), - ("LOW", "Low"), - ("MEDIUM", "Medium"), - ("HIGH", "High"), + ("No risk", "User has no risk"), + ("Low", "User has a low risk"), + ("Medium", "User has a medium risk"), + ("High", "User has a high risk"), ], default="No risk", help_text="Select the risk_score that users should have at least to send alert", @@ -90,15 +121,19 @@ class Migration(migrations.Migration): base_field=models.CharField( blank=True, choices=[ - ("NEW_DEVICE", "Login from new device"), - ("IMP_TRAVEL", "Impossible Travel detected"), - ("NEW_COUNTRY", "Login from new country"), - ("USER_RISK_THRESHOLD", "User risk threshold alert"), - ("LOGIN_ANONYMIZER_IP", "Login from anonymizer IP"), - ("ATYPICAL_COUNTRY", "Login from atypical country"), + ("New Device", "Login from new device"), + ("Imp Travel", "Impossible Travel detected"), + ("New Country", "Login from new country"), + ("User Risk Threshold", "User risk higher than threshold"), + ("Login Anonymizer Ip", "Login from an anonymizer IP"), + ( + "Atypical Country", + "Login from a country not visited recently", + ), ], max_length=50, ), + blank=True, default=list, help_text="List of alerts' types to exclude from the alerting", size=None, @@ -117,12 +152,12 @@ class Migration(migrations.Migration): name="name", field=models.CharField( choices=[ - ("NEW_DEVICE", "Login from new device"), - ("IMP_TRAVEL", "Impossible Travel detected"), - ("NEW_COUNTRY", "Login from new country"), - ("USER_RISK_THRESHOLD", "User risk threshold alert"), - ("LOGIN_ANONYMIZER_IP", "Login from anonymizer IP"), - ("ATYPICAL_COUNTRY", "Login from atypical country"), + ("New Device", "Login from new device"), + ("Imp Travel", "Impossible Travel detected"), + ("New Country", "Login from new country"), + ("User Risk Threshold", "User risk higher than threshold"), + ("Login Anonymizer Ip", "Login from an anonymizer IP"), + ("Atypical Country", "Login from a country not visited recently"), ], max_length=30, ), @@ -171,18 +206,144 @@ class Migration(migrations.Migration): size=None, ), ), + migrations.AlterField( + model_name="login", + name="timestamp", + field=models.DateTimeField(default=django.utils.timezone.now), + ), migrations.AlterField( model_name="user", name="risk_score", field=models.CharField( choices=[ - ("NO_RISK", "No risk"), - ("LOW", "Low"), - ("MEDIUM", "Medium"), - ("HIGH", "High"), + ("No risk", "User has no risk"), + ("Low", "User has a low risk"), + ("Medium", "User has a medium risk"), + ("High", "User has a high risk"), ], default="No risk", max_length=30, ), ), + migrations.AddConstraint( + model_name="alert", + constraint=models.CheckConstraint( + condition=models.Q( + ( + "name__in", + [ + "New Device", + "Imp Travel", + "New Country", + "User Risk Threshold", + "Login Anonymizer Ip", + "Atypical Country", + ], + ) + ), + name="valid_alert_name_choice", + ), + ), + migrations.AddConstraint( + model_name="alert", + constraint=models.CheckConstraint( + condition=models.Q( + ( + "filter_type__contained_by", + [ + ( + "isp_filter", + "Alert filtered because the ISP is whitelisted", + ), + ( + "is_mobile_filter", + "Alert filtered because login from a mobile device", + ), + ( + "is_vip_filter", + "Alert filtered because the user is not vip", + ), + ( + "allowed_country_filter", + "Alert filtered because the country is whitelisted", + ), + ( + "ignored_user_filter", + "Alert filtered because the user is ignored", + ), + ( + "alert_minimum_risk_score_filter", + "Alert filtered because the risk_score is lower than the threshold", + ), + ( + "filtered_alerts", + "Alert filtered because this detection type is excluded", + ), + ], + ) + ), + name="valid_alert_filter_type_choices", + ), + ), + migrations.AddConstraint( + model_name="config", + constraint=models.CheckConstraint( + condition=models.Q( + ( + "alert_minimum_risk_score__in", + ["No risk", "Low", "Medium", "High"], + ) + ), + name="valid_config_alert_minimum_risk_score_choice", + ), + ), + migrations.AddConstraint( + model_name="config", + constraint=models.CheckConstraint( + condition=models.Q( + ( + "filtered_alerts_types__contained_by", + [ + ( + "isp_filter", + "Alert filtered because the ISP is whitelisted", + ), + ( + "is_mobile_filter", + "Alert filtered because login from a mobile device", + ), + ( + "is_vip_filter", + "Alert filtered because the user is not vip", + ), + ( + "allowed_country_filter", + "Alert filtered because the country is whitelisted", + ), + ( + "ignored_user_filter", + "Alert filtered because the user is ignored", + ), + ( + "alert_minimum_risk_score_filter", + "Alert filtered because the risk_score is lower than the threshold", + ), + ( + "filtered_alerts", + "Alert filtered because this detection type is excluded", + ), + ], + ) + ), + name="valid_alert_filters_choices", + ), + ), + migrations.AddConstraint( + model_name="user", + constraint=models.CheckConstraint( + condition=models.Q(("risk_score__in", ["No risk", "Low", "Medium", "High"])), + name="valid_user_risk_score_choice", + ), + ), + migrations.RunPython(update_alert_name, migrations.RunPython.noop), ] diff --git a/buffalogs/impossible_travel/migrations/0012_remove_alert_valid_alert_filter_type_choices_and_more.py b/buffalogs/impossible_travel/migrations/0012_remove_alert_valid_alert_filter_type_choices_and_more.py new file mode 100644 index 0000000..093fc8d --- /dev/null +++ b/buffalogs/impossible_travel/migrations/0012_remove_alert_valid_alert_filter_type_choices_and_more.py @@ -0,0 +1,259 @@ +# Generated by Django 5.1.4 on 2024-12-23 10:24 + +import django.contrib.postgres.fields +import impossible_travel.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("impossible_travel", "0011_alert_filter_type_alert_is_filtered_and_more"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="alert", + name="valid_alert_filter_type_choices", + ), + migrations.RemoveConstraint( + model_name="config", + name="valid_alert_filters_choices", + ), + migrations.AddField( + model_name="config", + name="ignored_ISPs", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=50), + blank=True, + default=impossible_travel.models.get_default_ignored_ISPs, + help_text="List of ISPs names to remove from the detection", + null=True, + size=None, + ), + ), + migrations.AlterField( + model_name="alert", + name="filter_type", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + blank=True, + choices=[ + ( + "ignored_users filter", + "Alert filtered because the user is ignored - the user is in the Config.ignored_users list or Config.enabled_users list is populated", + ), + ( + "ignored_ips filter", + "Alert filtered because the IP is ignored - the ip is in the Config.ignored_ips list", + ), + ( + "allowed_countries filter", + "Alert filtered because the country is whitelisted - the country is in the Config.allowed_countries list", + ), + ( + "is_vip_filter", + "Alert filtered because the user is not vip - Config.alert_is_vip_only is True and the usre is not in the Config.vip_users list", + ), + ( + "alert_minimum_risk_score filter", + "Alert filtered because the User.risk_score is lower than the threshold set in Config.alert_minimum_risk_score", + ), + ( + "filtered_alerts_types filter", + "Alert filtered because this detection type is excluded - the Alert.name detection type is in the Config.filtered_alerts_types list", + ), + ( + "ignore_mobile_logins filter", + "Alert filtered because the login is from a mobile device - Config.ignore_mobile_logins is True", + ), + ( + "ignored_ISPs filter", + "Alert filtered because the ISP is whitelisted - The ISP is in the Config.ignored_ISPs list", + ), + ], + max_length=50, + ), + blank=True, + default=list, + help_text="List of filters that disabled the related alert", + size=None, + ), + ), + migrations.AlterField( + model_name="config", + name="allowed_countries", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=20), + blank=True, + default=impossible_travel.models.get_default_allowed_countries, + help_text="List of countries to exclude from the detection, because 'trusted' for the customer", + null=True, + size=None, + ), + ), + migrations.AlterField( + model_name="config", + name="enabled_users", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=50), + blank=True, + default=impossible_travel.models.get_default_enabled_users, + help_text="List of selected users on which the detection will perform", + null=True, + size=None, + ), + ), + migrations.AlterField( + model_name="config", + name="filtered_alerts_types", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + blank=True, + choices=[ + ("New Device", "Login from new device"), + ("Imp Travel", "Impossible Travel detected"), + ("New Country", "Login from new country"), + ("User Risk Threshold", "User risk higher than threshold"), + ("Login Anonymizer Ip", "Login from an anonymizer IP"), + ( + "Atypical Country", + "Login from a country not visited recently", + ), + ], + max_length=50, + ), + blank=True, + default=list, + help_text="List of alerts' types to exclude from the alerting", + null=True, + size=None, + ), + ), + migrations.AlterField( + model_name="config", + name="ignored_ips", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=50), + blank=True, + default=impossible_travel.models.get_default_ignored_ips, + help_text="List of IPs to remove from the detection", + null=True, + size=None, + ), + ), + migrations.AlterField( + model_name="config", + name="ignored_users", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=50), + blank=True, + default=impossible_travel.models.get_default_ignored_users, + help_text="List of users to be ignored from the detection", + null=True, + size=None, + ), + ), + migrations.AlterField( + model_name="config", + name="vip_users", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=50), + blank=True, + default=impossible_travel.models.get_default_vip_users, + help_text="List of users considered more sensitive", + null=True, + size=None, + ), + ), + migrations.AddConstraint( + model_name="alert", + constraint=models.CheckConstraint( + condition=models.Q( + ( + "filter_type__contained_by", + [ + ( + "ignored_users filter", + "Alert filtered because the user is ignored - the user is in the Config.ignored_users list or Config.enabled_users list is populated", + ), + ( + "ignored_ips filter", + "Alert filtered because the IP is ignored - the ip is in the Config.ignored_ips list", + ), + ( + "allowed_countries filter", + "Alert filtered because the country is whitelisted - the country is in the Config.allowed_countries list", + ), + ( + "is_vip_filter", + "Alert filtered because the user is not vip - Config.alert_is_vip_only is True and the usre is not in the Config.vip_users list", + ), + ( + "alert_minimum_risk_score filter", + "Alert filtered because the User.risk_score is lower than the threshold set in Config.alert_minimum_risk_score", + ), + ( + "filtered_alerts_types filter", + "Alert filtered because this detection type is excluded - the Alert.name detection type is in the Config.filtered_alerts_types list", + ), + ( + "ignore_mobile_logins filter", + "Alert filtered because the login is from a mobile device - Config.ignore_mobile_logins is True", + ), + ( + "ignored_ISPs filter", + "Alert filtered because the ISP is whitelisted - The ISP is in the Config.ignored_ISPs list", + ), + ], + ) + ), + name="valid_alert_filter_type_choices", + ), + ), + migrations.AddConstraint( + model_name="config", + constraint=models.CheckConstraint( + condition=models.Q( + ( + "filtered_alerts_types__contained_by", + [ + ( + "ignored_users filter", + "Alert filtered because the user is ignored - the user is in the Config.ignored_users list or Config.enabled_users list is populated", + ), + ( + "ignored_ips filter", + "Alert filtered because the IP is ignored - the ip is in the Config.ignored_ips list", + ), + ( + "allowed_countries filter", + "Alert filtered because the country is whitelisted - the country is in the Config.allowed_countries list", + ), + ( + "is_vip_filter", + "Alert filtered because the user is not vip - Config.alert_is_vip_only is True and the usre is not in the Config.vip_users list", + ), + ( + "alert_minimum_risk_score filter", + "Alert filtered because the User.risk_score is lower than the threshold set in Config.alert_minimum_risk_score", + ), + ( + "filtered_alerts_types filter", + "Alert filtered because this detection type is excluded - the Alert.name detection type is in the Config.filtered_alerts_types list", + ), + ( + "ignore_mobile_logins filter", + "Alert filtered because the login is from a mobile device - Config.ignore_mobile_logins is True", + ), + ( + "ignored_ISPs filter", + "Alert filtered because the ISP is whitelisted - The ISP is in the Config.ignored_ISPs list", + ), + ], + ) + ), + name="valid_alert_filters_choices", + ), + ), + ] diff --git a/buffalogs/impossible_travel/models.py b/buffalogs/impossible_travel/models.py index a04b290..275201b 100644 --- a/buffalogs/impossible_travel/models.py +++ b/buffalogs/impossible_travel/models.py @@ -2,24 +2,34 @@ from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.db import models +from django.utils import timezone from impossible_travel.constants import AlertDetectionType, AlertFilterType, UserRiskScoreType class User(models.Model): - risk_score = models.CharField(choices=UserRiskScoreType.choices(), max_length=30, null=False, default=UserRiskScoreType.NO_RISK.value) + risk_score = models.CharField(choices=UserRiskScoreType.choices, max_length=30, null=False, default=UserRiskScoreType.NO_RISK) username = models.TextField(unique=True, db_index=True) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) - def get_risk_display(self): - return AlertFilterType.get_risk_level(self.risk_level).name + def __str__(self): + return f"User object ({self.id}) - {self.username}" + + class Meta: + constraints = [ + models.CheckConstraint( + # Check that the User.risk_score is one of the value in the Enum UserRiskScoreType + check=models.Q(risk_score__in=[choice[0] for choice in UserRiskScoreType.choices]), + name="valid_user_risk_score_choice", + ) + ] class Login(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) - timestamp = models.DateTimeField() + timestamp = models.DateTimeField(default=timezone.now) latitude = models.FloatField(null=True) longitude = models.FloatField(null=True) country = models.TextField(blank=True) @@ -30,11 +40,7 @@ class Login(models.Model): class Alert(models.Model): - name = models.CharField( - choices=AlertDetectionType.choices(), - max_length=30, - null=False, - ) + name = models.CharField(choices=AlertDetectionType.choices, max_length=30, null=False, blank=False) user = models.ForeignKey(User, on_delete=models.CASCADE) login_raw_data = models.JSONField() created = models.DateTimeField(auto_now_add=True) @@ -43,12 +49,26 @@ class Alert(models.Model): is_vip = models.BooleanField(default=False) is_filtered = models.BooleanField(default=False, help_text="Show if the alert has been filtered because of some filter (listed in the filter_type field)") filter_type = ArrayField( - models.CharField(max_length=50, choices=AlertFilterType.choices(), blank=True), + models.CharField(max_length=50, choices=AlertFilterType.choices, blank=True), blank=True, default=list, help_text="List of filters that disabled the related alert", ) + class Meta: + constraints = [ + models.CheckConstraint( + # Check that the Alert.name is one of the value in the Enum AlertDetectionType + check=models.Q(name__in=[choice[0] for choice in AlertDetectionType.choices]), + name="valid_alert_name_choice", + ), + models.CheckConstraint( + # Check that each element in the Alert.filter_type is in the Enum AlertFilterType + check=models.Q(filter_type__contained_by=AlertFilterType.choices), + name="valid_alert_filter_type_choices", + ), + ] + class UsersIP(models.Model): created = models.DateTimeField(auto_now_add=True) @@ -77,6 +97,10 @@ def get_default_ignored_ips(): return list(settings.CERTEGO_BUFFALOGS_IGNORED_IPS) +def get_default_ignored_ISPs(): + return list(settings.CERTEGO_BUFFALOGS_IGNORED_ISPS) + + def get_default_allowed_countries(): return list(settings.CERTEGO_BUFFALOGS_ALLOWED_COUNTRIES) @@ -89,30 +113,44 @@ class Config(models.Model): created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) ignored_users = ArrayField( - models.CharField(max_length=50), blank=True, default=get_default_ignored_users, help_text="List of users to be ignored from the detection" + models.CharField(max_length=50), blank=True, null=True, default=get_default_ignored_users, help_text="List of users to be ignored from the detection" ) enabled_users = ArrayField( - models.CharField(max_length=50), blank=True, default=get_default_enabled_users, help_text="List of selected users on which the detection will perform" + models.CharField(max_length=50), + blank=True, + null=True, + default=get_default_enabled_users, + help_text="List of selected users on which the detection will perform", + ) + ignored_ips = ArrayField( + models.CharField(max_length=50), blank=True, null=True, default=get_default_ignored_ips, help_text="List of IPs to remove from the detection" + ) + ignored_ISPs = ArrayField( + models.CharField(max_length=50), blank=True, null=True, default=get_default_ignored_ISPs, help_text="List of ISPs names to remove from the detection" ) - ignored_ips = ArrayField(models.CharField(max_length=50), blank=True, default=get_default_ignored_ips, help_text="List of IPs to remove from the detection") allowed_countries = ArrayField( models.CharField(max_length=20), blank=True, + null=True, default=get_default_allowed_countries, help_text="List of countries to exclude from the detection, because 'trusted' for the customer", ) - vip_users = ArrayField(models.CharField(max_length=50), blank=True, default=get_default_vip_users, help_text="List of users considered more sensitive") + vip_users = ArrayField( + models.CharField(max_length=50), blank=True, null=True, default=get_default_vip_users, help_text="List of users considered more sensitive" + ) alert_is_vip_only = models.BooleanField(default=False, help_text="Flag to send alert only related to the users in the vip_users list") alert_minimum_risk_score = models.CharField( - choices=UserRiskScoreType.choices(), + choices=UserRiskScoreType.choices, max_length=30, blank=False, - default=UserRiskScoreType.NO_RISK.value, + default=UserRiskScoreType.NO_RISK, help_text="Select the risk_score that users should have at least to send alert", ) filtered_alerts_types = ArrayField( - models.CharField(max_length=50, choices=AlertDetectionType.choices(), blank=True), + models.CharField(max_length=50, choices=AlertDetectionType.choices, blank=True), default=list, + blank=True, + null=True, help_text="List of alerts' types to exclude from the alerting", ) ignore_mobile_logins = models.BooleanField(default=False, help_text="Flag to ignore mobile devices from the detection") @@ -145,3 +183,17 @@ def clean(self): def save(self, *args, **kwargs): self.clean() super().save(*args, **kwargs) + + class Meta: + constraints = [ + models.CheckConstraint( + # Check that the Config.alert_minimum_risk_score is one of the value in the Enum UserRiskScoreType + check=models.Q(alert_minimum_risk_score__in=[choice[0] for choice in UserRiskScoreType.choices]), + name="valid_config_alert_minimum_risk_score_choice", + ), + models.CheckConstraint( + # Check that each element in the Config.filtered_alerts_types is in the Enum AlertFilterType + check=models.Q(filtered_alerts_types__contained_by=AlertFilterType.choices), + name="valid_alert_filters_choices", + ), + ] diff --git a/buffalogs/impossible_travel/modules/impossible_travel.py b/buffalogs/impossible_travel/modules/impossible_travel.py index f189577..13a90b4 100644 --- a/buffalogs/impossible_travel/modules/impossible_travel.py +++ b/buffalogs/impossible_travel/modules/impossible_travel.py @@ -48,7 +48,7 @@ def calc_distance(self, db_user, prev_login, last_login_user_fields): alert_info["alert_name"] = AlertDetectionType.IMP_TRAVEL.value alert_info[ "alert_desc" - ] = f"{alert_info['alert_name']} for User: {db_user.username}, at: {last_timestamp_datetimeObj_aware}, from: {last_login_user_fields['country']}, previous country: {prev_login.country}, distance covered at {int(vel)} Km/h" + ] = f"{AlertDetectionType.IMP_TRAVEL.label} for User: {db_user.username}, at: {last_login_user_fields['timestamp']}, from: {last_login_user_fields['country']}, previous country: {prev_login.country}, distance covered at {int(vel)} Km/h" return alert_info, int(vel) def update_model(self, db_user, new_login): diff --git a/buffalogs/impossible_travel/modules/login_from_new_country.py b/buffalogs/impossible_travel/modules/login_from_new_country.py index 7e8ef2e..455f305 100644 --- a/buffalogs/impossible_travel/modules/login_from_new_country.py +++ b/buffalogs/impossible_travel/modules/login_from_new_country.py @@ -21,5 +21,5 @@ def check_country(self, db_user, login_field): if db_user.login_set.filter(country=new_country).count() == 0: time = login_field["timestamp"] alert_info["alert_name"] = AlertDetectionType.NEW_COUNTRY.value - alert_info["alert_desc"] = f"{alert_info['alert_name']} for User: {db_user.username}, at: {time}, from: {new_country}" + alert_info["alert_desc"] = f"{AlertDetectionType.NEW_COUNTRY.label} for User: {db_user.username}, at: {time}, from: {new_country}" return alert_info diff --git a/buffalogs/impossible_travel/modules/login_from_new_device.py b/buffalogs/impossible_travel/modules/login_from_new_device.py index 6b04ce4..6f1aa33 100644 --- a/buffalogs/impossible_travel/modules/login_from_new_device.py +++ b/buffalogs/impossible_travel/modules/login_from_new_device.py @@ -23,5 +23,5 @@ def check_new_device(self, db_user, login_field): if db_user.login_set.filter(user_agent=login_field["agent"]).count() == 0: timestamp = login_field["timestamp"] alert_info["alert_name"] = AlertDetectionType.NEW_DEVICE.value - alert_info["alert_desc"] = f"LOGIN FROM NEW DEVICE for User: {db_user.username}, at: {timestamp}" + alert_info["alert_desc"] = f"{AlertDetectionType.NEW_DEVICE.label} for User: {db_user.username}, at: {timestamp}" return alert_info diff --git a/buffalogs/impossible_travel/static/css/custom_admin.css b/buffalogs/impossible_travel/static/css/custom_admin.css new file mode 100644 index 0000000..44d128d --- /dev/null +++ b/buffalogs/impossible_travel/static/css/custom_admin.css @@ -0,0 +1,4 @@ +select[multiple] option:checked { + background-color: #007bff; + color: white; +} \ No newline at end of file diff --git a/buffalogs/impossible_travel/tests/test_impossible_travel.py b/buffalogs/impossible_travel/tests/test_impossible_travel.py index 2019f17..f9e1cc5 100644 --- a/buffalogs/impossible_travel/tests/test_impossible_travel.py +++ b/buffalogs/impossible_travel/tests/test_impossible_travel.py @@ -56,10 +56,11 @@ def test_calc_distance_alert(self): db_user = User.objects.get(username="Lorena Goldoni") prev_login = Login.objects.get(id=db_user.id) result, vel = self.imp_travel.calc_distance(db_user, prev_login, last_login_user_fields) - self.assertEqual("Impossible Travel detected", result["alert_name"]) - self.assertIn("for User: Lorena Goldoni", result["alert_desc"]) - self.assertIn("from: Sudan", result["alert_desc"]) - self.assertIn("previous country: United States, distance covered at 10109599 Km/h", result["alert_desc"]) + self.assertEqual("Imp Travel", result["alert_name"]) + self.assertEqual( + f"Impossible Travel detected for User: Lorena Goldoni, at: 2023-03-08T17:08:33.358Z, from: Sudan, previous country: United States, distance covered at {vel} Km/h", # noqa: E231 + result["alert_desc"], + ) def test_update_model(self): """Test update_model() function for unique login, so with same user_agent and country""" diff --git a/buffalogs/impossible_travel/tests/test_login_from_new_country.py b/buffalogs/impossible_travel/tests/test_login_from_new_country.py index 6d7fb67..86f05b0 100644 --- a/buffalogs/impossible_travel/tests/test_login_from_new_country.py +++ b/buffalogs/impossible_travel/tests/test_login_from_new_country.py @@ -45,4 +45,5 @@ def test_check_country_alert(self): "user_agent": "Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.9.1.8) Gecko/20100214 Ubuntu/9.10 (karmic) Firefox/3.5.8", } alert_result = self.new_country.check_country(db_user, last_login_user_fields) - self.assertEqual("Login from new country", alert_result["alert_name"]) + self.assertEqual("New Country", alert_result["alert_name"]) + self.assertEqual("Login from new country for User: Lorena Goldoni, at: 2023-03-08T17:10:33.358Z, from: Italy", alert_result["alert_desc"]) diff --git a/buffalogs/impossible_travel/tests/test_login_from_new_device.py b/buffalogs/impossible_travel/tests/test_login_from_new_device.py index 4ec088d..ff244f0 100644 --- a/buffalogs/impossible_travel/tests/test_login_from_new_device.py +++ b/buffalogs/impossible_travel/tests/test_login_from_new_device.py @@ -45,4 +45,5 @@ def test_check_new_device_alert(self): "agent": "Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.9.1.8) Gecko/20100214 Ubuntu/9.10 (karmic) Firefox/3.5.8", } alert_result = self.new_device.check_new_device(db_user, last_login_user_fields) - self.assertEqual("Login from new device", alert_result["alert_name"]) + self.assertEqual("New Device", alert_result["alert_name"]) + self.assertEqual("Login from new device for User: Lorena Goldoni, at: 2023-03-08T17:10:33.358Z", alert_result["alert_desc"]) diff --git a/buffalogs/impossible_travel/tests/test_tasks.py b/buffalogs/impossible_travel/tests/test_tasks.py index 4ef9560..77fa076 100644 --- a/buffalogs/impossible_travel/tests/test_tasks.py +++ b/buffalogs/impossible_travel/tests/test_tasks.py @@ -39,7 +39,7 @@ def test_clear_models_periodically(self): "timestamp": "2023-04-03T14:01:47.907Z", } UsersIP.objects.create(user=user_obj, ip=raw_data["ip"]) - Alert.objects.create(user=user_obj, login_raw_data=raw_data) + Alert.objects.create(user=user_obj, name=AlertDetectionType.NEW_COUNTRY.value, login_raw_data=raw_data) self.assertTrue(User.objects.filter(username="Lorena").exists()) self.assertTrue(Login.objects.filter(user=user_obj).exists()) self.assertTrue(Alert.objects.filter(user=user_obj).exists()) @@ -85,9 +85,9 @@ def test_update_risk_level_medium(self): db_user = User.objects.get(username="Lorena Goldoni") Alert.objects.bulk_create( [ - Alert(user=db_user, name=AlertDetectionType.IMP_TRAVEL.value, login_raw_data="Test1", description="Test_Description1"), - Alert(user=db_user, name=AlertDetectionType.NEW_DEVICE.value, login_raw_data="Test2", description="Test_Description2"), - Alert(user=db_user, name=AlertDetectionType.NEW_COUNTRY.value, login_raw_data="Test3", description="Test_Description3"), + Alert(user=db_user, name=AlertDetectionType.IMP_TRAVEL, login_raw_data="Test1", description="Test_Description1"), + Alert(user=db_user, name=AlertDetectionType.NEW_DEVICE, login_raw_data="Test2", description="Test_Description2"), + Alert(user=db_user, name=AlertDetectionType.NEW_COUNTRY, login_raw_data="Test3", description="Test_Description3"), ] ) tasks.update_risk_level() @@ -101,11 +101,11 @@ def test_update_risk_level_high(self): db_user = User.objects.get(username="Lorena Goldoni") Alert.objects.bulk_create( [ - Alert(user=db_user, name=AlertDetectionType.IMP_TRAVEL.value, login_raw_data="Test1", description="Test_Description1"), - Alert(user=db_user, name=AlertDetectionType.NEW_DEVICE.value, login_raw_data="Test2", description="Test_Description2"), - Alert(user=db_user, name=AlertDetectionType.NEW_COUNTRY.value, login_raw_data="Test3", description="Test_Description3"), - Alert(user=db_user, name=AlertDetectionType.NEW_COUNTRY.value, login_raw_data="Test4", description="Test_Description4"), - Alert(user=db_user, name=AlertDetectionType.NEW_COUNTRY.value, login_raw_data="Test5", description="Test_Description5"), + Alert(user=db_user, name=AlertDetectionType.IMP_TRAVEL, login_raw_data="Test1", description="Test_Description1"), + Alert(user=db_user, name=AlertDetectionType.NEW_DEVICE, login_raw_data="Test2", description="Test_Description2"), + Alert(user=db_user, name=AlertDetectionType.NEW_COUNTRY, login_raw_data="Test3", description="Test_Description3"), + Alert(user=db_user, name=AlertDetectionType.NEW_COUNTRY, login_raw_data="Test4", description="Test_Description4"), + Alert(user=db_user, name=AlertDetectionType.NEW_COUNTRY, login_raw_data="Test5", description="Test_Description5"), ] ) tasks.update_risk_level() @@ -118,7 +118,7 @@ def test_set_alert(self): db_login = Login.objects.get(user_agent="Mozilla/5.0 (X11;U; Linux i686; en-GB; rv:1.9.1) Gecko/20090624 Ubuntu/9.04 (jaunty) Firefox/3.5") timestamp = db_login.timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ") login_data = {"timestamp": timestamp, "latitude": "45.4758", "longitude": "9.2275", "country": db_login.country, "agent": db_login.user_agent} - name = AlertDetectionType.IMP_TRAVEL.value + name = AlertDetectionType.IMP_TRAVEL desc = f"{name} for User: {db_user.username}, \ at: {timestamp}, from: ({db_login.latitude}, {db_login.longitude})" alert_info = { @@ -126,9 +126,9 @@ def test_set_alert(self): "alert_desc": desc, } tasks.set_alert(db_user, login_data, alert_info) - db_alert = Alert.objects.get(user=db_user, name=AlertDetectionType.IMP_TRAVEL.value) + db_alert = Alert.objects.get(user=db_user, name=AlertDetectionType.IMP_TRAVEL) self.assertIsNotNone(db_alert) - self.assertEqual("Impossible Travel detected", db_alert.name) + self.assertEqual("Imp Travel", db_alert.name) self.assertFalse(db_alert.is_vip) def test_set_alert_vip_user(self): @@ -137,7 +137,7 @@ def test_set_alert_vip_user(self): db_login = Login.objects.filter(user=db_user).first() timestamp = db_login.timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ") login_data = {"timestamp": timestamp, "latitude": "45.4758", "longitude": "9.2275", "country": db_login.country, "agent": db_login.user_agent} - name = AlertDetectionType.IMP_TRAVEL.value + name = AlertDetectionType.IMP_TRAVEL desc = f"{name} for User: {db_user.username}, \ at: {timestamp}, from: ({db_login.latitude}, {db_login.longitude})" alert_info = { @@ -145,7 +145,7 @@ def test_set_alert_vip_user(self): "alert_desc": desc, } tasks.set_alert(db_user, login_data, alert_info) - db_alert = Alert.objects.get(user=db_user, name=AlertDetectionType.IMP_TRAVEL.value) + db_alert = Alert.objects.get(user=db_user, name=AlertDetectionType.IMP_TRAVEL) self.assertTrue(db_alert.is_vip) def test_process_logs_data_lost(self): @@ -261,7 +261,7 @@ def test_check_fields_logins(self): self.assertEqual(57, Login.objects.get(user=db_user, country="Japan").timestamp.minute) self.assertEqual(27, Login.objects.get(user=db_user, country="Japan").timestamp.second) - def check_fields_alerts(self): + def test_check_fields_alerts(self): fields1 = load_test_data("test_check_fields_part1") fields2 = load_test_data("test_check_fields_part2") db_user = User.objects.get(username="Aisha Delgado") @@ -275,9 +275,36 @@ def check_fields_alerts(self): # 5. at 2023-05-03T06:57:27.768Z alert IMP TRAVEL # 6. at 2023-05-03T07:10:23.154Z alert IMP TRAVEL self.assertEqual(6, Alert.objects.filter(user=db_user).count()) - self.assertEqual(2, Alert.objects.filter(user=db_user, name=AlertDetectionType.NEW_DEVICE.value).count()) - self.assertEqual(1, Alert.objects.filter(user=db_user, name=AlertDetectionType.NEW_COUNTRY.value).count()) - self.assertEqual(3, Alert.objects.filter(user=db_user, name=AlertDetectionType.IMP_TRAVEL.value).count()) + new_device_alerts_fields1 = Alert.objects.filter(user=db_user, name=AlertDetectionType.NEW_DEVICE) + self.assertEqual(2, new_device_alerts_fields1.count()) + new_country_alerts_fields1 = Alert.objects.filter(user=db_user, name=AlertDetectionType.NEW_COUNTRY) + self.assertEqual(1, new_country_alerts_fields1.count()) + imp_travel_alerts_fields1 = Alert.objects.filter(user=db_user, name=AlertDetectionType.IMP_TRAVEL) + self.assertEqual(3, imp_travel_alerts_fields1.count()) + # check new_device alerts for fields1 logins + self.assertEqual("New Device", new_device_alerts_fields1[0].name) + self.assertEqual("Login from new device for User: Aisha Delgado, at: 2023-05-03T06:55:31.768Z", new_device_alerts_fields1[0].description) + self.assertEqual("New Device", new_device_alerts_fields1[1].name) + self.assertEqual("Login from new device for User: Aisha Delgado, at: 2023-05-03T06:57:27.768Z", new_device_alerts_fields1[1].description) + # check new_country alerts for fields1 logins + self.assertEqual("New Country", new_country_alerts_fields1[0].name) + self.assertEqual("Login from new country for User: Aisha Delgado, at: 2023-05-03T06:57:27.768Z, from: Japan", new_country_alerts_fields1[0].description) + # check imp_travel alerts for fields1 logins + self.assertEqual("Imp Travel", imp_travel_alerts_fields1[0].name) + self.assertEqual( + "Impossible Travel detected for User: Aisha Delgado, at: 2023-05-03T06:55:31.768Z, from: United States, previous country: India, distance covered at 133973 Km/h", + imp_travel_alerts_fields1[0].description, + ) + self.assertEqual("Imp Travel", imp_travel_alerts_fields1[1].name) + self.assertEqual( + "Impossible Travel detected for User: Aisha Delgado, at: 2023-05-03T06:57:27.768Z, from: Japan, previous country: United States, distance covered at 344009 Km/h", + imp_travel_alerts_fields1[1].description, + ) + self.assertEqual("Imp Travel", imp_travel_alerts_fields1[2].name) + self.assertEqual( + "Impossible Travel detected for User: Aisha Delgado, at: 2023-05-03T07:10:23.154Z, from: United States, previous country: Japan, distance covered at 52564 Km/h", + imp_travel_alerts_fields1[2].description, + ) self.assertEqual(0, Alert.objects.filter(user=db_user, is_vip=True).count()) # Adding "Aisha Delgado" to vip users @@ -290,12 +317,28 @@ def check_fields_alerts(self): # 9. at 2023-05-03T07:18:38.768Z alert NEW DEVICE # 10. at 2023-05-03T07:18:38.768Z alert IMP TRAVEL # 11. at 2023-05-03T07:20:36.154Z alert IMP TRAVEL + + # get IDs of old alerts to check the new alerts + new_device_alerts_fields1_ids = list(new_device_alerts_fields1.values_list("id", flat=True)) + tasks.check_fields(db_user, fields2) - self.assertEqual(4, Alert.objects.filter(user=db_user, name=AlertDetectionType.NEW_DEVICE.value).count()) - self.assertEqual(1, Alert.objects.filter(user=db_user, name=AlertDetectionType.NEW_COUNTRY.value).count()) - self.assertEqual(6, Alert.objects.filter(user=db_user, name=AlertDetectionType.IMP_TRAVEL.value).count()) - self.assertEqual(5, Alert.objects.filter(user=db_user, is_vip=True).count()) self.assertEqual(11, Alert.objects.filter(user=db_user).count()) + # get new_device alerts relating to fields2 making query all_new_device_alerts - new_device_alerts_fields1 + all_new_device_alerts = Alert.objects.filter(user=db_user, name=AlertDetectionType.NEW_DEVICE) + self.assertEqual(4, all_new_device_alerts.count()) + new_device_alerts_fields2 = all_new_device_alerts.exclude(id__in=new_device_alerts_fields1_ids) + self.assertEqual(2, new_device_alerts_fields2.count()) + # check new_device alerts for fields2 + self.assertEqual("New Device", new_device_alerts_fields2[0].name) + self.assertEqual("Login from new device for User: Aisha Delgado, at: 2023-05-03T07:14:22.768Z", new_device_alerts_fields2[0].description) + self.assertEqual("New Device", new_device_alerts_fields2[1].name) + self.assertEqual("Login from new device for User: Aisha Delgado, at: 2023-05-03T07:18:38.768Z", new_device_alerts_fields2[1].description) + + # same old new_country alert relating to fields1 logins + self.assertEqual(1, Alert.objects.filter(user=db_user, name=AlertDetectionType.NEW_COUNTRY).count()) + + self.assertEqual(6, Alert.objects.filter(user=db_user, name=AlertDetectionType.IMP_TRAVEL).count()) + self.assertEqual(5, Alert.objects.filter(user=db_user, is_vip=True).count()) def test_check_fields_usersip(self): db_user = User.objects.get(username="Aisha Delgado") diff --git a/buffalogs/impossible_travel/tests/test_views.py b/buffalogs/impossible_travel/tests/test_views.py index 6eea8df..f6a26ea 100644 --- a/buffalogs/impossible_travel/tests/test_views.py +++ b/buffalogs/impossible_travel/tests/test_views.py @@ -13,11 +13,11 @@ def setUp(self): self.client = Client() User.objects.bulk_create( [ - User(username="Lorena Goldoni", risk_score=UserRiskScoreType.NO_RISK.value), - User(username="Lorygold", risk_score=UserRiskScoreType.LOW.value), - User(username="Lory", risk_score=UserRiskScoreType.LOW.value), - User(username="Lor", risk_score=UserRiskScoreType.LOW.value), - User(username="Loryg", risk_score=UserRiskScoreType.MEDIUM.value), + User(username="Lorena Goldoni", risk_score=UserRiskScoreType.NO_RISK), + User(username="Lorygold", risk_score=UserRiskScoreType.LOW), + User(username="Lory", risk_score=UserRiskScoreType.LOW), + User(username="Lor", risk_score=UserRiskScoreType.LOW), + User(username="Loryg", risk_score=UserRiskScoreType.MEDIUM), ] ) db_user = User.objects.get(username="Lorena Goldoni") @@ -73,7 +73,7 @@ def setUp(self): [ Alert( user=db_user, - name=AlertDetectionType.NEW_DEVICE.value, + name=AlertDetectionType.NEW_DEVICE, login_raw_data={ "id": "ht9DEIgBnkLiMp6r-SG-", "ip": "203.0.113.24", @@ -88,7 +88,7 @@ def setUp(self): ), Alert( user=db_user, - name=AlertDetectionType.IMP_TRAVEL.value, + name=AlertDetectionType.IMP_TRAVEL, login_raw_data={ "id": "vfraw14gw", "ip": "1.2.3.4", @@ -103,7 +103,7 @@ def setUp(self): ), Alert( user=db_user, - name=AlertDetectionType.IMP_TRAVEL.value, + name=AlertDetectionType.IMP_TRAVEL, login_raw_data={ "id": "vfraw14gw", "ip": "1.2.3.4", @@ -118,7 +118,7 @@ def setUp(self): ), Alert( user=db_user, - name=AlertDetectionType.IMP_TRAVEL.value, + name=AlertDetectionType.IMP_TRAVEL, login_raw_data={ "id": "vfraw14gw", "ip": "1.2.3.4", @@ -133,7 +133,7 @@ def setUp(self): ), Alert( user=db_user, - name=AlertDetectionType.NEW_DEVICE.value, + name=AlertDetectionType.NEW_DEVICE, login_raw_data={ "id": "ht9DEIgBnkLiMp6r-SG-", "ip": "203.0.113.24", @@ -148,7 +148,7 @@ def setUp(self): ), Alert( user=db_user, - name=AlertDetectionType.NEW_DEVICE.value, + name=AlertDetectionType.NEW_DEVICE, login_raw_data={ "id": "ht9DEIgBnkLiMp6r-SG-", "ip": "203.0.113.24", @@ -219,8 +219,8 @@ def test_alerts_api(self): start = creation_mock_time end = creation_mock_time + timedelta(minutes=10) list_expected_result = [ - {"timestamp": "2023-06-20T10:17:33.358Z", "username": "Lorena Goldoni", "rule_name": "Impossible Travel detected"}, - {"timestamp": "2023-05-20T11:45:01.229Z", "username": "Lorena Goldoni", "rule_name": "Login from new device"}, + {"timestamp": "2023-06-20T10:17:33.358Z", "username": "Lorena Goldoni", "rule_name": "Imp Travel"}, + {"timestamp": "2023-05-20T11:45:01.229Z", "username": "Lorena Goldoni", "rule_name": "New Device"}, ] response = self.client.get(f"{reverse('alerts_api')}?start={start.strftime('%Y-%m-%dT%H:%M:%SZ')}&end={end.strftime('%Y-%m-%dT%H:%M:%SZ')}") self.assertEqual(response.status_code, 200) From bca641c64339a94b26c76016b40e5870d788f9e1 Mon Sep 17 00:00:00 2001 From: Lorygold Date: Mon, 23 Dec 2024 12:41:53 +0100 Subject: [PATCH 07/15] Updated CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f3cacf..be28570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ### 1.3.1 ### Changes * Forced the existence of only 1 Config object with id=1 +* Added Config.ignored_ISPs field for filtering known ISPs IPs +* Added forms: UserAdminForm, AlertAdminForm and ConfigAdminForm +* Added ShortLabelChoiceField to customize ChoiceField in order to show the short_value as label on DjangoValue +* Added MultiChoiceArrayField to customize ArrayField in order to support multiple choices +* Created MultiChoiceArrayWidget widget for user-friendly interface for ArrayField with multiple choices on Django Admin #### Bugfix * Fixed alert.name representation enums ### 1.3.0 From 967c6e647f6aa1a81c658230fcf01b9ebb7fbfdc Mon Sep 17 00:00:00 2001 From: Lorygold Date: Mon, 23 Dec 2024 12:44:38 +0100 Subject: [PATCH 08/15] Version 1.3.1 --- django-buffalogs/buffalogs.egg-info/PKG-INFO | 2 +- django-buffalogs/buffalogs.egg-info/SOURCES.txt | 3 +++ django-buffalogs/setup.cfg | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/django-buffalogs/buffalogs.egg-info/PKG-INFO b/django-buffalogs/buffalogs.egg-info/PKG-INFO index 3af920d..58edd11 100644 --- a/django-buffalogs/buffalogs.egg-info/PKG-INFO +++ b/django-buffalogs/buffalogs.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: buffalogs -Version: 1.3.0 +Version: 1.3.1 Summary: A Django app to detect anomaly logins. Author: Lorena Goldoni License: Apache-2.0 diff --git a/django-buffalogs/buffalogs.egg-info/SOURCES.txt b/django-buffalogs/buffalogs.egg-info/SOURCES.txt index a7853dc..98a0b0d 100644 --- a/django-buffalogs/buffalogs.egg-info/SOURCES.txt +++ b/django-buffalogs/buffalogs.egg-info/SOURCES.txt @@ -14,12 +14,14 @@ impossible_travel/__init__.py impossible_travel/admin.py impossible_travel/apps.py impossible_travel/constants.py +impossible_travel/forms.py impossible_travel/models.py impossible_travel/tasks.py impossible_travel/views.py impossible_travel/management/commands/clear_models.py impossible_travel/management/commands/impossible_travel.py impossible_travel/management/commands/setup_config.py +impossible_travel/management/commands/__pycache__/setup_config.cpython-312.pyc impossible_travel/migrations/0001_initial.py impossible_travel/migrations/0002_alert_updated.py impossible_travel/migrations/0003_alter_alert_updated.py @@ -31,6 +33,7 @@ impossible_travel/migrations/0008_usersip.py impossible_travel/migrations/0009_config_ignored_ips_config_ignored_users_and_more.py impossible_travel/migrations/0010_config_alert_max_days_config_distance_accepted_and_more.py impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py +impossible_travel/migrations/0012_remove_alert_valid_alert_filter_type_choices_and_more.py impossible_travel/migrations/__init__.py impossible_travel/modules/impossible_travel.py impossible_travel/modules/login_from_new_country.py diff --git a/django-buffalogs/setup.cfg b/django-buffalogs/setup.cfg index a4e24b7..03e03da 100644 --- a/django-buffalogs/setup.cfg +++ b/django-buffalogs/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = buffalogs -version = 1.3.0 +version = 1.3.1 description = A Django app to detect anomaly logins. long_description = file: README.rst author = Lorena Goldoni From 2c18792b1e8378d9ca4d6dbe1162daa7af10825d Mon Sep 17 00:00:00 2001 From: Lorena Goldoni <33703137+Lorygold@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:21:49 +0100 Subject: [PATCH 09/15] Updated some Python dapendencies (#110) * Updated some Python dapendencies * fix --- .../python_linters/requirements-linters.txt | 15 +- CHANGELOG.md | 1 + ..._filter_type_alert_is_filtered_and_more.py | 126 ++++++--- ...alid_alert_filter_type_choices_and_more.py | 259 ------------------ buffalogs/requirements.txt | 17 +- buffalogs/requirements_dev.txt | 6 +- django-buffalogs/setup.cfg | 17 +- 7 files changed, 107 insertions(+), 334 deletions(-) delete mode 100644 buffalogs/impossible_travel/migrations/0012_remove_alert_valid_alert_filter_type_choices_and_more.py diff --git a/.github/configurations/python_linters/requirements-linters.txt b/.github/configurations/python_linters/requirements-linters.txt index 8d831b6..a9ec6e4 100644 --- a/.github/configurations/python_linters/requirements-linters.txt +++ b/.github/configurations/python_linters/requirements-linters.txt @@ -1,8 +1,7 @@ -black==22.3.0 -isort==5.12.0 -flake8==5.0.4 -flake8-django==1.1.5 -pylint==2.14.3 -pylint-django==2.5.3 -bandit==1.7.4 -autoflake==1.7.7 \ No newline at end of file +autoflake==2.3.1 +bandit==1.7.9 +black==24.8.0 +flake8==7.1.1 +isort==5.13.2 +pylint==3.2.6 +pylint-django==2.5.5 diff --git a/CHANGELOG.md b/CHANGELOG.md index be28570..c485d39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Added ShortLabelChoiceField to customize ChoiceField in order to show the short_value as label on DjangoValue * Added MultiChoiceArrayField to customize ArrayField in order to support multiple choices * Created MultiChoiceArrayWidget widget for user-friendly interface for ArrayField with multiple choices on Django Admin +* Updated some Python dependecies #### Bugfix * Fixed alert.name representation enums ### 1.3.0 diff --git a/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py b/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py index 29dd2eb..263c4b6 100644 --- a/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py +++ b/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py @@ -1,10 +1,10 @@ -# Generated by Django 5.1.4 on 2024-12-22 17:54 +# Generated by Django 4.2.16 on 2024-12-23 13:58 import django.contrib.postgres.fields +from django.db import migrations, models import django.utils.timezone import impossible_travel.models from impossible_travel.constants import AlertDetectionType -from django.db import migrations, models import logging logger = logging.getLogger(__name__) @@ -41,27 +41,37 @@ class Migration(migrations.Migration): base_field=models.CharField( blank=True, choices=[ - ("isp_filter", "Alert filtered because the ISP is whitelisted"), ( - "is_mobile_filter", - "Alert filtered because login from a mobile device", + "ignored_users filter", + "Alert filtered because the user is ignored - the user is in the Config.ignored_users list or Config.enabled_users list is populated", + ), + ( + "ignored_ips filter", + "Alert filtered because the IP is ignored - the ip is in the Config.ignored_ips list", + ), + ( + "allowed_countries filter", + "Alert filtered because the country is whitelisted - the country is in the Config.allowed_countries list", + ), + ( + "is_vip_filter", + "Alert filtered because the user is not vip - Config.alert_is_vip_only is True and the usre is not in the Config.vip_users list", ), - ("is_vip_filter", "Alert filtered because the user is not vip"), ( - "allowed_country_filter", - "Alert filtered because the country is whitelisted", + "alert_minimum_risk_score filter", + "Alert filtered because the User.risk_score is lower than the threshold set in Config.alert_minimum_risk_score", ), ( - "ignored_user_filter", - "Alert filtered because the user is ignored", + "filtered_alerts_types filter", + "Alert filtered because this detection type is excluded - the Alert.name detection type is in the Config.filtered_alerts_types list", ), ( - "alert_minimum_risk_score_filter", - "Alert filtered because the risk_score is lower than the threshold", + "ignore_mobile_logins filter", + "Alert filtered because the login is from a mobile device - Config.ignore_mobile_logins is True", ), ( - "filtered_alerts", - "Alert filtered because this detection type is excluded", + "ignored_ISPs filter", + "Alert filtered because the ISP is whitelisted - The ISP is in the Config.ignored_ISPs list", ), ], max_length=50, @@ -111,6 +121,7 @@ class Migration(migrations.Migration): blank=True, default=impossible_travel.models.get_default_enabled_users, help_text="List of selected users on which the detection will perform", + null=True, size=None, ), ), @@ -136,6 +147,7 @@ class Migration(migrations.Migration): blank=True, default=list, help_text="List of alerts' types to exclude from the alerting", + null=True, size=None, ), ), @@ -147,6 +159,18 @@ class Migration(migrations.Migration): help_text="Flag to ignore mobile devices from the detection", ), ), + migrations.AddField( + model_name="config", + name="ignored_ISPs", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=50), + blank=True, + default=impossible_travel.models.get_default_ignored_ISPs, + help_text="List of ISPs names to remove from the detection", + null=True, + size=None, + ), + ), migrations.AlterField( model_name="alert", name="name", @@ -170,6 +194,7 @@ class Migration(migrations.Migration): blank=True, default=impossible_travel.models.get_default_allowed_countries, help_text="List of countries to exclude from the detection, because 'trusted' for the customer", + null=True, size=None, ), ), @@ -181,6 +206,7 @@ class Migration(migrations.Migration): blank=True, default=impossible_travel.models.get_default_ignored_ips, help_text="List of IPs to remove from the detection", + null=True, size=None, ), ), @@ -192,6 +218,7 @@ class Migration(migrations.Migration): blank=True, default=impossible_travel.models.get_default_ignored_users, help_text="List of users to be ignored from the detection", + null=True, size=None, ), ), @@ -203,6 +230,7 @@ class Migration(migrations.Migration): blank=True, default=impossible_travel.models.get_default_vip_users, help_text="List of users considered more sensitive", + null=True, size=None, ), ), @@ -228,7 +256,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="alert", constraint=models.CheckConstraint( - condition=models.Q( + check=models.Q( ( "name__in", [ @@ -247,37 +275,41 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="alert", constraint=models.CheckConstraint( - condition=models.Q( + check=models.Q( ( "filter_type__contained_by", [ ( - "isp_filter", - "Alert filtered because the ISP is whitelisted", + "ignored_users filter", + "Alert filtered because the user is ignored - the user is in the Config.ignored_users list or Config.enabled_users list is populated", + ), + ( + "ignored_ips filter", + "Alert filtered because the IP is ignored - the ip is in the Config.ignored_ips list", ), ( - "is_mobile_filter", - "Alert filtered because login from a mobile device", + "allowed_countries filter", + "Alert filtered because the country is whitelisted - the country is in the Config.allowed_countries list", ), ( "is_vip_filter", - "Alert filtered because the user is not vip", + "Alert filtered because the user is not vip - Config.alert_is_vip_only is True and the usre is not in the Config.vip_users list", ), ( - "allowed_country_filter", - "Alert filtered because the country is whitelisted", + "alert_minimum_risk_score filter", + "Alert filtered because the User.risk_score is lower than the threshold set in Config.alert_minimum_risk_score", ), ( - "ignored_user_filter", - "Alert filtered because the user is ignored", + "filtered_alerts_types filter", + "Alert filtered because this detection type is excluded - the Alert.name detection type is in the Config.filtered_alerts_types list", ), ( - "alert_minimum_risk_score_filter", - "Alert filtered because the risk_score is lower than the threshold", + "ignore_mobile_logins filter", + "Alert filtered because the login is from a mobile device - Config.ignore_mobile_logins is True", ), ( - "filtered_alerts", - "Alert filtered because this detection type is excluded", + "ignored_ISPs filter", + "Alert filtered because the ISP is whitelisted - The ISP is in the Config.ignored_ISPs list", ), ], ) @@ -288,7 +320,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="config", constraint=models.CheckConstraint( - condition=models.Q( + check=models.Q( ( "alert_minimum_risk_score__in", ["No risk", "Low", "Medium", "High"], @@ -300,37 +332,41 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="config", constraint=models.CheckConstraint( - condition=models.Q( + check=models.Q( ( "filtered_alerts_types__contained_by", [ ( - "isp_filter", - "Alert filtered because the ISP is whitelisted", + "ignored_users filter", + "Alert filtered because the user is ignored - the user is in the Config.ignored_users list or Config.enabled_users list is populated", + ), + ( + "ignored_ips filter", + "Alert filtered because the IP is ignored - the ip is in the Config.ignored_ips list", ), ( - "is_mobile_filter", - "Alert filtered because login from a mobile device", + "allowed_countries filter", + "Alert filtered because the country is whitelisted - the country is in the Config.allowed_countries list", ), ( "is_vip_filter", - "Alert filtered because the user is not vip", + "Alert filtered because the user is not vip - Config.alert_is_vip_only is True and the usre is not in the Config.vip_users list", ), ( - "allowed_country_filter", - "Alert filtered because the country is whitelisted", + "alert_minimum_risk_score filter", + "Alert filtered because the User.risk_score is lower than the threshold set in Config.alert_minimum_risk_score", ), ( - "ignored_user_filter", - "Alert filtered because the user is ignored", + "filtered_alerts_types filter", + "Alert filtered because this detection type is excluded - the Alert.name detection type is in the Config.filtered_alerts_types list", ), ( - "alert_minimum_risk_score_filter", - "Alert filtered because the risk_score is lower than the threshold", + "ignore_mobile_logins filter", + "Alert filtered because the login is from a mobile device - Config.ignore_mobile_logins is True", ), ( - "filtered_alerts", - "Alert filtered because this detection type is excluded", + "ignored_ISPs filter", + "Alert filtered because the ISP is whitelisted - The ISP is in the Config.ignored_ISPs list", ), ], ) @@ -341,7 +377,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="user", constraint=models.CheckConstraint( - condition=models.Q(("risk_score__in", ["No risk", "Low", "Medium", "High"])), + check=models.Q(("risk_score__in", ["No risk", "Low", "Medium", "High"])), name="valid_user_risk_score_choice", ), ), diff --git a/buffalogs/impossible_travel/migrations/0012_remove_alert_valid_alert_filter_type_choices_and_more.py b/buffalogs/impossible_travel/migrations/0012_remove_alert_valid_alert_filter_type_choices_and_more.py deleted file mode 100644 index 093fc8d..0000000 --- a/buffalogs/impossible_travel/migrations/0012_remove_alert_valid_alert_filter_type_choices_and_more.py +++ /dev/null @@ -1,259 +0,0 @@ -# Generated by Django 5.1.4 on 2024-12-23 10:24 - -import django.contrib.postgres.fields -import impossible_travel.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("impossible_travel", "0011_alert_filter_type_alert_is_filtered_and_more"), - ] - - operations = [ - migrations.RemoveConstraint( - model_name="alert", - name="valid_alert_filter_type_choices", - ), - migrations.RemoveConstraint( - model_name="config", - name="valid_alert_filters_choices", - ), - migrations.AddField( - model_name="config", - name="ignored_ISPs", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=50), - blank=True, - default=impossible_travel.models.get_default_ignored_ISPs, - help_text="List of ISPs names to remove from the detection", - null=True, - size=None, - ), - ), - migrations.AlterField( - model_name="alert", - name="filter_type", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.CharField( - blank=True, - choices=[ - ( - "ignored_users filter", - "Alert filtered because the user is ignored - the user is in the Config.ignored_users list or Config.enabled_users list is populated", - ), - ( - "ignored_ips filter", - "Alert filtered because the IP is ignored - the ip is in the Config.ignored_ips list", - ), - ( - "allowed_countries filter", - "Alert filtered because the country is whitelisted - the country is in the Config.allowed_countries list", - ), - ( - "is_vip_filter", - "Alert filtered because the user is not vip - Config.alert_is_vip_only is True and the usre is not in the Config.vip_users list", - ), - ( - "alert_minimum_risk_score filter", - "Alert filtered because the User.risk_score is lower than the threshold set in Config.alert_minimum_risk_score", - ), - ( - "filtered_alerts_types filter", - "Alert filtered because this detection type is excluded - the Alert.name detection type is in the Config.filtered_alerts_types list", - ), - ( - "ignore_mobile_logins filter", - "Alert filtered because the login is from a mobile device - Config.ignore_mobile_logins is True", - ), - ( - "ignored_ISPs filter", - "Alert filtered because the ISP is whitelisted - The ISP is in the Config.ignored_ISPs list", - ), - ], - max_length=50, - ), - blank=True, - default=list, - help_text="List of filters that disabled the related alert", - size=None, - ), - ), - migrations.AlterField( - model_name="config", - name="allowed_countries", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=20), - blank=True, - default=impossible_travel.models.get_default_allowed_countries, - help_text="List of countries to exclude from the detection, because 'trusted' for the customer", - null=True, - size=None, - ), - ), - migrations.AlterField( - model_name="config", - name="enabled_users", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=50), - blank=True, - default=impossible_travel.models.get_default_enabled_users, - help_text="List of selected users on which the detection will perform", - null=True, - size=None, - ), - ), - migrations.AlterField( - model_name="config", - name="filtered_alerts_types", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.CharField( - blank=True, - choices=[ - ("New Device", "Login from new device"), - ("Imp Travel", "Impossible Travel detected"), - ("New Country", "Login from new country"), - ("User Risk Threshold", "User risk higher than threshold"), - ("Login Anonymizer Ip", "Login from an anonymizer IP"), - ( - "Atypical Country", - "Login from a country not visited recently", - ), - ], - max_length=50, - ), - blank=True, - default=list, - help_text="List of alerts' types to exclude from the alerting", - null=True, - size=None, - ), - ), - migrations.AlterField( - model_name="config", - name="ignored_ips", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=50), - blank=True, - default=impossible_travel.models.get_default_ignored_ips, - help_text="List of IPs to remove from the detection", - null=True, - size=None, - ), - ), - migrations.AlterField( - model_name="config", - name="ignored_users", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=50), - blank=True, - default=impossible_travel.models.get_default_ignored_users, - help_text="List of users to be ignored from the detection", - null=True, - size=None, - ), - ), - migrations.AlterField( - model_name="config", - name="vip_users", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=50), - blank=True, - default=impossible_travel.models.get_default_vip_users, - help_text="List of users considered more sensitive", - null=True, - size=None, - ), - ), - migrations.AddConstraint( - model_name="alert", - constraint=models.CheckConstraint( - condition=models.Q( - ( - "filter_type__contained_by", - [ - ( - "ignored_users filter", - "Alert filtered because the user is ignored - the user is in the Config.ignored_users list or Config.enabled_users list is populated", - ), - ( - "ignored_ips filter", - "Alert filtered because the IP is ignored - the ip is in the Config.ignored_ips list", - ), - ( - "allowed_countries filter", - "Alert filtered because the country is whitelisted - the country is in the Config.allowed_countries list", - ), - ( - "is_vip_filter", - "Alert filtered because the user is not vip - Config.alert_is_vip_only is True and the usre is not in the Config.vip_users list", - ), - ( - "alert_minimum_risk_score filter", - "Alert filtered because the User.risk_score is lower than the threshold set in Config.alert_minimum_risk_score", - ), - ( - "filtered_alerts_types filter", - "Alert filtered because this detection type is excluded - the Alert.name detection type is in the Config.filtered_alerts_types list", - ), - ( - "ignore_mobile_logins filter", - "Alert filtered because the login is from a mobile device - Config.ignore_mobile_logins is True", - ), - ( - "ignored_ISPs filter", - "Alert filtered because the ISP is whitelisted - The ISP is in the Config.ignored_ISPs list", - ), - ], - ) - ), - name="valid_alert_filter_type_choices", - ), - ), - migrations.AddConstraint( - model_name="config", - constraint=models.CheckConstraint( - condition=models.Q( - ( - "filtered_alerts_types__contained_by", - [ - ( - "ignored_users filter", - "Alert filtered because the user is ignored - the user is in the Config.ignored_users list or Config.enabled_users list is populated", - ), - ( - "ignored_ips filter", - "Alert filtered because the IP is ignored - the ip is in the Config.ignored_ips list", - ), - ( - "allowed_countries filter", - "Alert filtered because the country is whitelisted - the country is in the Config.allowed_countries list", - ), - ( - "is_vip_filter", - "Alert filtered because the user is not vip - Config.alert_is_vip_only is True and the usre is not in the Config.vip_users list", - ), - ( - "alert_minimum_risk_score filter", - "Alert filtered because the User.risk_score is lower than the threshold set in Config.alert_minimum_risk_score", - ), - ( - "filtered_alerts_types filter", - "Alert filtered because this detection type is excluded - the Alert.name detection type is in the Config.filtered_alerts_types list", - ), - ( - "ignore_mobile_logins filter", - "Alert filtered because the login is from a mobile device - Config.ignore_mobile_logins is True", - ), - ( - "ignored_ISPs filter", - "Alert filtered because the ISP is whitelisted - The ISP is in the Config.ignored_ISPs list", - ), - ], - ) - ), - name="valid_alert_filters_choices", - ), - ), - ] diff --git a/buffalogs/requirements.txt b/buffalogs/requirements.txt index 2d27d35..31e8529 100644 --- a/buffalogs/requirements.txt +++ b/buffalogs/requirements.txt @@ -1,23 +1,22 @@ -celery>=5.2.7 +celery>=5.4.0 certifi>=2022.9.24 cfgv>=3.3.1 distlib>=0.3.6 -Django>=4.1.4 -djangorestframework>=3.14.0 +Django==4.2.16 +djangorestframework>=3.15.2 djangorestframework-simplejwt>=5.3.0 django-cors-headers>=4.3.0 django-environ>=0.9.0 -djangorestframework>=3.14.0 -elasticsearch>=7.17.7 -elasticsearch-dsl>=7.4.0 +elasticsearch>=7.17.12 +elasticsearch-dsl>=7.4.1 filelock>=3.9.0 geographiclib>=2.0 -geopy>=2.3.0 +geopy>=2.4.1 kombu>=5.2.4 nodeenv>=1.7.0 pathspec>=0.10.3 prompt-toolkit>=3.0.33 -psycopg[binary]>=3.1.12 +psycopg[binary]>=3.2.3 pygal>=3.0.0 pygal-maps-world>=1.0.2 python-dateutil>=2.8.2 @@ -25,6 +24,6 @@ python-dotenv>=0.21.0 pytz>=2024.1 PyYAML>=6.0 urllib3>=1.26.12 -uWSGI>=2.0.21 +uWSGI>=2.0.28 virtualenv>=20.17.1 wcwidth>=0.2.5 diff --git a/buffalogs/requirements_dev.txt b/buffalogs/requirements_dev.txt index 4c24ce1..ec719ff 100644 --- a/buffalogs/requirements_dev.txt +++ b/buffalogs/requirements_dev.txt @@ -1,6 +1,4 @@ -pre-commit==2.21.0 +pre-commit>=3.8.0 mypy-extensions>=0.4.3 -flake8>=3.8.4 -flake8-django>=1.1.5 ipython>=8.30.0 -pipdeptree>=2.16.0 \ No newline at end of file +pipdeptree>=2.23.1 \ No newline at end of file diff --git a/django-buffalogs/setup.cfg b/django-buffalogs/setup.cfg index 03e03da..b109fa6 100644 --- a/django-buffalogs/setup.cfg +++ b/django-buffalogs/setup.cfg @@ -16,26 +16,25 @@ include_package_data = true packages = find: python_requires = >=3.8 install_requires = - celery>=5.2.7 + celery>=5.4.0 certifi>=2022.9.24 cfgv>=3.3.1 distlib>=0.3.6 - Django>=4.1.4 - djangorestframework>=3.14.0 + Django>=4.2.16 + djangorestframework>=3.15.2 djangorestframework-simplejwt>=5.3.0 django-cors-headers>=4.3.0 django-environ>=0.9.0 - djangorestframework>=3.14.0 - elasticsearch>=7.17.7 - elasticsearch-dsl>=7.4.0 + elasticsearch>=7.17.12 + elasticsearch-dsl>=7.4.1 filelock>=3.9.0 geographiclib>=2.0 - geopy>=2.3.0 + geopy>=2.4.1 kombu>=5.2.4 nodeenv>=1.7.0 pathspec>=0.10.3 prompt-toolkit>=3.0.33 - psycopg>=3.1.12 + psycopg>=3.2.3 psycopg-binary>=3.1.12 pygal>=3.0.0 pygal-maps-world>=1.0.2 @@ -43,6 +42,6 @@ install_requires = python-dotenv>=0.21.0 PyYAML>=6.0 urllib3>=1.26.12 - uWSGI>=2.0.21 + uWSGI>=2.0.28 virtualenv>=20.17.1 wcwidth>=0.2.5 From 8bb10d9b4acee73e7cd44b41ae842f02be3c8768 Mon Sep 17 00:00:00 2001 From: Lorygold Date: Mon, 23 Dec 2024 15:35:05 +0100 Subject: [PATCH 10/15] Version 1.3.1 fixed --- django-buffalogs/buffalogs.egg-info/PKG-INFO | 17 ++++++++--------- django-buffalogs/buffalogs.egg-info/SOURCES.txt | 1 - .../buffalogs.egg-info/requires.txt | 17 ++++++++--------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/django-buffalogs/buffalogs.egg-info/PKG-INFO b/django-buffalogs/buffalogs.egg-info/PKG-INFO index 58edd11..d597eed 100644 --- a/django-buffalogs/buffalogs.egg-info/PKG-INFO +++ b/django-buffalogs/buffalogs.egg-info/PKG-INFO @@ -11,26 +11,25 @@ Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Software Development Requires-Python: >=3.8 License-File: LICENSE.txt -Requires-Dist: celery>=5.2.7 +Requires-Dist: celery>=5.4.0 Requires-Dist: certifi>=2022.9.24 Requires-Dist: cfgv>=3.3.1 Requires-Dist: distlib>=0.3.6 -Requires-Dist: Django>=4.1.4 -Requires-Dist: djangorestframework>=3.14.0 +Requires-Dist: Django>=4.2.16 +Requires-Dist: djangorestframework>=3.15.2 Requires-Dist: djangorestframework-simplejwt>=5.3.0 Requires-Dist: django-cors-headers>=4.3.0 Requires-Dist: django-environ>=0.9.0 -Requires-Dist: djangorestframework>=3.14.0 -Requires-Dist: elasticsearch>=7.17.7 -Requires-Dist: elasticsearch-dsl>=7.4.0 +Requires-Dist: elasticsearch>=7.17.12 +Requires-Dist: elasticsearch-dsl>=7.4.1 Requires-Dist: filelock>=3.9.0 Requires-Dist: geographiclib>=2.0 -Requires-Dist: geopy>=2.3.0 +Requires-Dist: geopy>=2.4.1 Requires-Dist: kombu>=5.2.4 Requires-Dist: nodeenv>=1.7.0 Requires-Dist: pathspec>=0.10.3 Requires-Dist: prompt-toolkit>=3.0.33 -Requires-Dist: psycopg>=3.1.12 +Requires-Dist: psycopg>=3.2.3 Requires-Dist: psycopg-binary>=3.1.12 Requires-Dist: pygal>=3.0.0 Requires-Dist: pygal-maps-world>=1.0.2 @@ -38,7 +37,7 @@ Requires-Dist: python-dateutil>=2.8.2 Requires-Dist: python-dotenv>=0.21.0 Requires-Dist: PyYAML>=6.0 Requires-Dist: urllib3>=1.26.12 -Requires-Dist: uWSGI>=2.0.21 +Requires-Dist: uWSGI>=2.0.28 Requires-Dist: virtualenv>=20.17.1 Requires-Dist: wcwidth>=0.2.5 diff --git a/django-buffalogs/buffalogs.egg-info/SOURCES.txt b/django-buffalogs/buffalogs.egg-info/SOURCES.txt index 98a0b0d..6bbda9c 100644 --- a/django-buffalogs/buffalogs.egg-info/SOURCES.txt +++ b/django-buffalogs/buffalogs.egg-info/SOURCES.txt @@ -33,7 +33,6 @@ impossible_travel/migrations/0008_usersip.py impossible_travel/migrations/0009_config_ignored_ips_config_ignored_users_and_more.py impossible_travel/migrations/0010_config_alert_max_days_config_distance_accepted_and_more.py impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py -impossible_travel/migrations/0012_remove_alert_valid_alert_filter_type_choices_and_more.py impossible_travel/migrations/__init__.py impossible_travel/modules/impossible_travel.py impossible_travel/modules/login_from_new_country.py diff --git a/django-buffalogs/buffalogs.egg-info/requires.txt b/django-buffalogs/buffalogs.egg-info/requires.txt index 4fbf065..f57cbed 100644 --- a/django-buffalogs/buffalogs.egg-info/requires.txt +++ b/django-buffalogs/buffalogs.egg-info/requires.txt @@ -1,23 +1,22 @@ -celery>=5.2.7 +celery>=5.4.0 certifi>=2022.9.24 cfgv>=3.3.1 distlib>=0.3.6 -Django>=4.1.4 -djangorestframework>=3.14.0 +Django>=4.2.16 +djangorestframework>=3.15.2 djangorestframework-simplejwt>=5.3.0 django-cors-headers>=4.3.0 django-environ>=0.9.0 -djangorestframework>=3.14.0 -elasticsearch>=7.17.7 -elasticsearch-dsl>=7.4.0 +elasticsearch>=7.17.12 +elasticsearch-dsl>=7.4.1 filelock>=3.9.0 geographiclib>=2.0 -geopy>=2.3.0 +geopy>=2.4.1 kombu>=5.2.4 nodeenv>=1.7.0 pathspec>=0.10.3 prompt-toolkit>=3.0.33 -psycopg>=3.1.12 +psycopg>=3.2.3 psycopg-binary>=3.1.12 pygal>=3.0.0 pygal-maps-world>=1.0.2 @@ -25,6 +24,6 @@ python-dateutil>=2.8.2 python-dotenv>=0.21.0 PyYAML>=6.0 urllib3>=1.26.12 -uWSGI>=2.0.21 +uWSGI>=2.0.28 virtualenv>=20.17.1 wcwidth>=0.2.5 From 0d84eb04470395571b4fe1201bd062c216999232 Mon Sep 17 00:00:00 2001 From: Lorena Goldoni <33703137+Lorygold@users.noreply.github.com> Date: Tue, 7 Jan 2025 14:49:15 +0100 Subject: [PATCH 11/15] Fix migration 0011 (#112) * Fixed migration 0011 operations order * Updated CHANGELOG.md --- CHANGELOG.md | 3 +++ buffalogs/buffalogs/settings/settings.py | 1 + buffalogs/buffalogs/urls.py | 1 + .../0011_alert_filter_type_alert_is_filtered_and_more.py | 2 +- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 556fe1d..dc3c9d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ ## 1.3.x +### 1.3.2 +#### Bugfix +* Fixed migration 0011 ### 1.3.1 ### Changes * Forced the existence of only 1 Config object with id=1 diff --git a/buffalogs/buffalogs/settings/settings.py b/buffalogs/buffalogs/settings/settings.py index 9e9aaa1..32d01f7 100644 --- a/buffalogs/buffalogs/settings/settings.py +++ b/buffalogs/buffalogs/settings/settings.py @@ -9,6 +9,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.1/ref/settings/ """ + import os from datetime import timedelta from pathlib import Path diff --git a/buffalogs/buffalogs/urls.py b/buffalogs/buffalogs/urls.py index 40997d1..d358e79 100644 --- a/buffalogs/buffalogs/urls.py +++ b/buffalogs/buffalogs/urls.py @@ -13,6 +13,7 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.contrib import admin from django.urls import include, path from impossible_travel import views diff --git a/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py b/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py index 263c4b6..f9d21f8 100644 --- a/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py +++ b/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py @@ -34,6 +34,7 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RunPython(update_alert_name, migrations.RunPython.noop), migrations.AddField( model_name="alert", name="filter_type", @@ -381,5 +382,4 @@ class Migration(migrations.Migration): name="valid_user_risk_score_choice", ), ), - migrations.RunPython(update_alert_name, migrations.RunPython.noop), ] From ecc8be8f3eb10518ace092538c99aa0055fcf9a1 Mon Sep 17 00:00:00 2001 From: Lorygold Date: Tue, 7 Jan 2025 15:44:17 +0100 Subject: [PATCH 12/15] Version 1.3.2 --- django-buffalogs/buffalogs.egg-info/PKG-INFO | 2 +- django-buffalogs/setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/django-buffalogs/buffalogs.egg-info/PKG-INFO b/django-buffalogs/buffalogs.egg-info/PKG-INFO index d597eed..811b39b 100644 --- a/django-buffalogs/buffalogs.egg-info/PKG-INFO +++ b/django-buffalogs/buffalogs.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: buffalogs -Version: 1.3.1 +Version: 1.3.2 Summary: A Django app to detect anomaly logins. Author: Lorena Goldoni License: Apache-2.0 diff --git a/django-buffalogs/setup.cfg b/django-buffalogs/setup.cfg index b109fa6..bc6117b 100644 --- a/django-buffalogs/setup.cfg +++ b/django-buffalogs/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = buffalogs -version = 1.3.1 +version = 1.3.2 description = A Django app to detect anomaly logins. long_description = file: README.rst author = Lorena Goldoni From 3b73a23afb1d04ce3183aa142ea133e5241108d3 Mon Sep 17 00:00:00 2001 From: Lorygold Date: Tue, 7 Jan 2025 16:24:09 +0100 Subject: [PATCH 13/15] typo --- .../0011_alert_filter_type_alert_is_filtered_and_more.py | 1 - 1 file changed, 1 deletion(-) diff --git a/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py b/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py index 1b45359..16656b7 100644 --- a/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py +++ b/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py @@ -158,7 +158,6 @@ class Migration(migrations.Migration): default=impossible_travel.models.get_default_enabled_users, help_text="List of selected users on which the detection will perform", null=True, - null=True, size=None, ), ), From 323e6ec3ec71486d1213506234ceb60f939c927e Mon Sep 17 00:00:00 2001 From: Lorena Goldoni <33703137+Lorygold@users.noreply.github.com> Date: Tue, 7 Jan 2025 17:47:53 +0100 Subject: [PATCH 14/15] Removed "version" property from docker-compose files because obsolete (#114) --- CHANGELOG.md | 2 + ..._filter_type_alert_is_filtered_and_more.py | 79 +------------------ docker-compose.elastic.yaml | 2 - docker-compose.override.yaml | 2 - docker-compose.yaml | 2 - 5 files changed, 3 insertions(+), 84 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc3c9d4..815b5ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## 1.3.x ### 1.3.2 +### Changes +* Removed "version" property from docker-compose files because it is obsolete now #### Bugfix * Fixed migration 0011 ### 1.3.1 diff --git a/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py b/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py index 16656b7..1c6b876 100644 --- a/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py +++ b/buffalogs/impossible_travel/migrations/0011_alert_filter_type_alert_is_filtered_and_more.py @@ -1,5 +1,4 @@ -# Generated by Django 4.2.16 on 2024-12-23 13:58 -# Generated by Django 4.2.16 on 2024-12-23 13:58 +# Generated by Django 4.2.16 on 2025-01-07 16:35 import django.contrib.postgres.fields from django.db import migrations, models @@ -60,28 +59,6 @@ class Migration(migrations.Migration): "Alert filtered because the user is not vip - Config.alert_is_vip_only is True and the usre is not in the Config.vip_users list", ), ( - "ignored_users filter", - "Alert filtered because the user is ignored - the user is in the Config.ignored_users list or Config.enabled_users list is populated", - ), - ( - "ignored_ips filter", - "Alert filtered because the IP is ignored - the ip is in the Config.ignored_ips list", - ), - ( - "allowed_countries filter", - "Alert filtered because the country is whitelisted - the country is in the Config.allowed_countries list", - ), - ( - "is_vip_filter", - "Alert filtered because the user is not vip - Config.alert_is_vip_only is True and the usre is not in the Config.vip_users list", - ), - ( - "alert_minimum_risk_score filter", - "Alert filtered because the User.risk_score is lower than the threshold set in Config.alert_minimum_risk_score", - ), - ( - "filtered_alerts_types filter", - "Alert filtered because this detection type is excluded - the Alert.name detection type is in the Config.filtered_alerts_types list", "alert_minimum_risk_score filter", "Alert filtered because the User.risk_score is lower than the threshold set in Config.alert_minimum_risk_score", ), @@ -97,14 +74,6 @@ class Migration(migrations.Migration): "ignored_ISPs filter", "Alert filtered because the ISP is whitelisted - The ISP is in the Config.ignored_ISPs list", ), - ( - "ignore_mobile_logins filter", - "Alert filtered because the login is from a mobile device - Config.ignore_mobile_logins is True", - ), - ( - "ignored_ISPs filter", - "Alert filtered because the ISP is whitelisted - The ISP is in the Config.ignored_ISPs list", - ), ], max_length=50, ), @@ -139,10 +108,6 @@ class Migration(migrations.Migration): ("Low", "User has a low risk"), ("Medium", "User has a medium risk"), ("High", "User has a high risk"), - ("No risk", "User has no risk"), - ("Low", "User has a low risk"), - ("Medium", "User has a medium risk"), - ("High", "User has a high risk"), ], default="No risk", help_text="Select the risk_score that users should have at least to send alert", @@ -177,24 +142,13 @@ class Migration(migrations.Migration): "Atypical Country", "Login from a country not visited recently", ), - ("New Device", "Login from new device"), - ("Imp Travel", "Impossible Travel detected"), - ("New Country", "Login from new country"), - ("User Risk Threshold", "User risk higher than threshold"), - ("Login Anonymizer Ip", "Login from an anonymizer IP"), - ( - "Atypical Country", - "Login from a country not visited recently", - ), ], max_length=50, ), blank=True, - blank=True, default=list, help_text="List of alerts' types to exclude from the alerting", null=True, - null=True, size=None, ), ), @@ -218,18 +172,6 @@ class Migration(migrations.Migration): size=None, ), ), - migrations.AddField( - model_name="config", - name="ignored_ISPs", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=50), - blank=True, - default=impossible_travel.models.get_default_ignored_ISPs, - help_text="List of ISPs names to remove from the detection", - null=True, - size=None, - ), - ), migrations.AlterField( model_name="alert", name="name", @@ -241,12 +183,6 @@ class Migration(migrations.Migration): ("User Risk Threshold", "User risk higher than threshold"), ("Login Anonymizer Ip", "Login from an anonymizer IP"), ("Atypical Country", "Login from a country not visited recently"), - ("New Device", "Login from new device"), - ("Imp Travel", "Impossible Travel detected"), - ("New Country", "Login from new country"), - ("User Risk Threshold", "User risk higher than threshold"), - ("Login Anonymizer Ip", "Login from an anonymizer IP"), - ("Atypical Country", "Login from a country not visited recently"), ], max_length=30, ), @@ -260,7 +196,6 @@ class Migration(migrations.Migration): default=impossible_travel.models.get_default_allowed_countries, help_text="List of countries to exclude from the detection, because 'trusted' for the customer", null=True, - null=True, size=None, ), ), @@ -273,7 +208,6 @@ class Migration(migrations.Migration): default=impossible_travel.models.get_default_ignored_ips, help_text="List of IPs to remove from the detection", null=True, - null=True, size=None, ), ), @@ -286,7 +220,6 @@ class Migration(migrations.Migration): default=impossible_travel.models.get_default_ignored_users, help_text="List of users to be ignored from the detection", null=True, - null=True, size=None, ), ), @@ -299,7 +232,6 @@ class Migration(migrations.Migration): default=impossible_travel.models.get_default_vip_users, help_text="List of users considered more sensitive", null=True, - null=True, size=None, ), ), @@ -308,11 +240,6 @@ class Migration(migrations.Migration): name="timestamp", field=models.DateTimeField(default=django.utils.timezone.now), ), - migrations.AlterField( - model_name="login", - name="timestamp", - field=models.DateTimeField(default=django.utils.timezone.now), - ), migrations.AlterField( model_name="user", name="risk_score", @@ -322,10 +249,6 @@ class Migration(migrations.Migration): ("Low", "User has a low risk"), ("Medium", "User has a medium risk"), ("High", "User has a high risk"), - ("No risk", "User has no risk"), - ("Low", "User has a low risk"), - ("Medium", "User has a medium risk"), - ("High", "User has a high risk"), ], default="No risk", max_length=30, diff --git a/docker-compose.elastic.yaml b/docker-compose.elastic.yaml index 3eefcc5..7a7317d 100644 --- a/docker-compose.elastic.yaml +++ b/docker-compose.elastic.yaml @@ -1,5 +1,3 @@ -version: '3.2' - services: elasticsearch: container_name: buffalogs_elasticsearch diff --git a/docker-compose.override.yaml b/docker-compose.override.yaml index 1c14d20..14a08ae 100644 --- a/docker-compose.override.yaml +++ b/docker-compose.override.yaml @@ -1,5 +1,3 @@ -version: '3.2' - services: buffalogs: volumes: diff --git a/docker-compose.yaml b/docker-compose.yaml index 63d9954..e358ced 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,3 @@ -version: '3.2' - services: buffalogs_postgres: From efc2e74d7578c5c6fb96c083fd8cff6a82e5ff54 Mon Sep 17 00:00:00 2001 From: Lorygold Date: Tue, 7 Jan 2025 17:49:54 +0100 Subject: [PATCH 15/15] fixed package version 1.3.2 --- django-buffalogs/buffalogs.egg-info/PKG-INFO | 8 -------- 1 file changed, 8 deletions(-) diff --git a/django-buffalogs/buffalogs.egg-info/PKG-INFO b/django-buffalogs/buffalogs.egg-info/PKG-INFO index 0a20296..811b39b 100644 --- a/django-buffalogs/buffalogs.egg-info/PKG-INFO +++ b/django-buffalogs/buffalogs.egg-info/PKG-INFO @@ -12,31 +12,24 @@ Classifier: Topic :: Software Development Requires-Python: >=3.8 License-File: LICENSE.txt Requires-Dist: celery>=5.4.0 -Requires-Dist: celery>=5.4.0 Requires-Dist: certifi>=2022.9.24 Requires-Dist: cfgv>=3.3.1 Requires-Dist: distlib>=0.3.6 Requires-Dist: Django>=4.2.16 Requires-Dist: djangorestframework>=3.15.2 -Requires-Dist: Django>=4.2.16 -Requires-Dist: djangorestframework>=3.15.2 Requires-Dist: djangorestframework-simplejwt>=5.3.0 Requires-Dist: django-cors-headers>=4.3.0 Requires-Dist: django-environ>=0.9.0 Requires-Dist: elasticsearch>=7.17.12 Requires-Dist: elasticsearch-dsl>=7.4.1 -Requires-Dist: elasticsearch>=7.17.12 -Requires-Dist: elasticsearch-dsl>=7.4.1 Requires-Dist: filelock>=3.9.0 Requires-Dist: geographiclib>=2.0 Requires-Dist: geopy>=2.4.1 -Requires-Dist: geopy>=2.4.1 Requires-Dist: kombu>=5.2.4 Requires-Dist: nodeenv>=1.7.0 Requires-Dist: pathspec>=0.10.3 Requires-Dist: prompt-toolkit>=3.0.33 Requires-Dist: psycopg>=3.2.3 -Requires-Dist: psycopg>=3.2.3 Requires-Dist: psycopg-binary>=3.1.12 Requires-Dist: pygal>=3.0.0 Requires-Dist: pygal-maps-world>=1.0.2 @@ -45,7 +38,6 @@ Requires-Dist: python-dotenv>=0.21.0 Requires-Dist: PyYAML>=6.0 Requires-Dist: urllib3>=1.26.12 Requires-Dist: uWSGI>=2.0.28 -Requires-Dist: uWSGI>=2.0.28 Requires-Dist: virtualenv>=20.17.1 Requires-Dist: wcwidth>=0.2.5