Skip to content

Commit

Permalink
1.3.1 (#111)
Browse files Browse the repository at this point in the history
* 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

* 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

* 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

* Version 1.3.0

* Force one config object (#107)

* Forced only 1 Config object presence

* Set always Config.id=1

* Updated CHANGELOG.md

* 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

* Updated CHANGELOG.md

* Version 1.3.1

* Updated some Python dapendencies (#110)

* Updated some Python dapendencies

* fix

* Version 1.3.1 fixed
  • Loading branch information
Lorygold authored Dec 23, 2024
1 parent 542727c commit ffff049
Show file tree
Hide file tree
Showing 24 changed files with 619 additions and 194 deletions.
4 changes: 0 additions & 4 deletions .github/configurations/python_linters/.flake8
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,3 @@ ignore =
exclude =
*/migrations/*,
Dockerfile

per-file-ignores =
# imported but unused
certego.py: E231
15 changes: 7 additions & 8 deletions .github/configurations/python_linters/requirements-linters.txt
Original file line number Diff line number Diff line change
@@ -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
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
## 1.3.x
### 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
* Updated some Python dependencies
#### Bugfix
* Fixed alert.name representation enums
### 1.3.0
#### Feature
* Added configuration panel in order to set custom preferences
Expand Down
5 changes: 3 additions & 2 deletions buffalogs/buffalogs/settings/certego.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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/")
Expand All @@ -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}")
38 changes: 34 additions & 4 deletions buffalogs/impossible_travel/admin.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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")


Expand Down
89 changes: 46 additions & 43 deletions buffalogs/impossible_travel/constants.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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")
78 changes: 78 additions & 0 deletions buffalogs/impossible_travel/forms.py
Original file line number Diff line number Diff line change
@@ -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",),
}
Loading

0 comments on commit ffff049

Please sign in to comment.