Skip to content

Commit 8de0446

Browse files
Frontend - analyzables page + user events modal (#2931)
* Frontend - Analyzables page (#2865) * adjusted menu * analyzables page draft + refactor * adjusted table columns and serializer * frontend tests * tests * prettier * fix * changes * fix * Analyzable overview page (#2883) * analyzable overview page draft * refactor * visualizers draft * Analyzable overview page * prettier * removed cache * changes * adjusted tests * fixed decay date visualizer * use enum --------- Co-authored-by: drosetti <d.rosetti@certego.net> * added data model tab (#2900) * Added 'user reports' in the history page (#2895) * added user-reports page * fix * changes * tests * changes * prettier * fixed decayed badge * fix * User events modal form (#2928) * form draft * adjusted backend * adjusted analyzable history * fix * fixed evaluation modal * renamed user reports to user events * renamed analyzable to artifact in the gui * adjusted frontend tests * fix * frontend tests * fix frontend tests * prettier * fix * fix * fix * refactor history tables * fix --------- Co-authored-by: drosetti <d.rosetti@certego.net>
1 parent 7bdc3b9 commit 8de0446

82 files changed

Lines changed: 5026 additions & 1734 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import rest_framework_filters as filters
2+
from django_filters.widgets import QueryArrayWidget
3+
4+
from api_app.analyzables_manager.models import Analyzable
5+
6+
7+
class CharInFilter(filters.BaseInFilter, filters.CharFilter):
8+
pass
9+
10+
11+
class AnalyzableFilter(filters.FilterSet):
12+
name = CharInFilter(widget=QueryArrayWidget)
13+
14+
class Meta:
15+
model = Analyzable
16+
fields = {
17+
"discovery_date": ["lte", "gte"],
18+
}

api_app/analyzables_manager/models.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,7 @@
77

88
from api_app.analyzables_manager.queryset import AnalyzableQuerySet
99
from api_app.choices import Classification
10-
from api_app.data_model_manager.models import (
11-
BaseDataModel,
12-
DomainDataModel,
13-
FileDataModel,
14-
IPDataModel,
15-
)
10+
from api_app.data_model_manager.models import BaseDataModel
1611
from api_app.data_model_manager.queryset import BaseDataModelQuerySet
1712
from api_app.defaults import file_directory_path
1813
from api_app.helpers import calculate_md5, calculate_sha1, calculate_sha256
@@ -85,20 +80,7 @@ def get_all_user_events_data_model(
8580
return self.get_data_model_class().objects.filter(query)
8681

8782
def get_data_model_class(self) -> Type[BaseDataModel]:
88-
if self.classification == Classification.IP.value:
89-
return IPDataModel
90-
elif self.classification in [
91-
Classification.URL.value,
92-
Classification.DOMAIN.value,
93-
]:
94-
return DomainDataModel
95-
elif self.classification in [
96-
Classification.HASH.value,
97-
Classification.FILE.value,
98-
]:
99-
return FileDataModel
100-
else:
101-
raise NotImplementedError()
83+
return self.CLASSIFICATIONS.get_data_model_class(self.classification)
10284

10385
def _set_hashes(self, value: Union[str, bytes]):
10486
if isinstance(value, str):
@@ -111,6 +93,10 @@ def _set_hashes(self, value: Union[str, bytes]):
11193
self.sha1 = calculate_sha1(value)
11294

11395
def clean(self):
96+
if self.file:
97+
self.classification = Classification.FILE.value
98+
else:
99+
self.classification = Classification.calculate_observable(self.name)
114100
if self.classification == Classification.FILE.value:
115101
from api_app.analyzers_manager.models import MimeTypes
116102

api_app/analyzables_manager/queryset.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,21 @@ class AnalyzableQuerySet(QuerySet):
1010
def visible_for_user(self, user):
1111

1212
from api_app.models import Job
13+
from api_app.user_events_manager.models import UserAnalyzableEvent
1314

14-
analyzables = (
15+
analyzables_job = (
1516
Job.objects.visible_for_user(user)
1617
.values("analyzable")
1718
.distinct()
1819
.values_list("analyzable__pk", flat=True)
1920
)
20-
return self.filter(pk__in=analyzables)
21+
analyzables_ue = (
22+
UserAnalyzableEvent.objects.visible_for_user(user)
23+
.values("analyzable")
24+
.distinct()
25+
.values_list("analyzable__pk", flat=True)
26+
)
27+
return self.filter(pk__in=analyzables_job) | self.filter(pk__in=analyzables_ue)
2128

2229
def create(self, *args, **kwargs):
2330
obj = self.model(**kwargs)
Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,58 @@
1-
from rest_framework.serializers import ModelSerializer
1+
from rest_framework import serializers as rfs
22

33
from api_app.analyzables_manager.models import Analyzable
4+
from api_app.choices import Classification
5+
from api_app.models import Job
46
from api_app.serializers.job import JobRelatedField
57

68

7-
class AnalyzableSerializer(ModelSerializer):
9+
class AnalyzableSerializer(rfs.ModelSerializer):
810
jobs = JobRelatedField(many=True, read_only=True)
911

1012
class Meta:
1113
model = Analyzable
1214
fields = "__all__"
15+
read_only_fields = [
16+
"jobs",
17+
"discovery_date",
18+
"md5",
19+
"classification",
20+
"sha256",
21+
"sha1",
22+
"mimetype",
23+
]
24+
25+
def to_representation(self, instance):
26+
analyzable = super().to_representation(instance)
27+
job = (
28+
Job.objects.filter(id__in=analyzable["jobs"])
29+
.order_by("-finished_analysis_time")
30+
.first()
31+
)
32+
user_event_data_model = (
33+
instance.get_all_user_events_data_model().order_by("-date").first()
34+
)
35+
36+
if job is None and user_event_data_model is None:
37+
analyzable["last_data_model"] = None
38+
return analyzable
39+
elif job is not None and user_event_data_model is not None:
40+
last_data_model = (
41+
job.data_model
42+
if job.data_model.date > user_event_data_model.date
43+
else user_event_data_model
44+
)
45+
else:
46+
last_data_model = (
47+
job.data_model if job is not None else user_event_data_model
48+
)
49+
50+
serializer_class = Classification.get_data_model_class(
51+
classification=analyzable["classification"],
52+
).get_serializer()
53+
analyzable["last_data_model"] = serializer_class(last_data_model).data
54+
return analyzable
55+
56+
def create(self, validated_data):
57+
instance, _ = self.Meta.model.objects.get_or_create(**validated_data)
58+
return instance
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,76 @@
1+
import logging
2+
from http import HTTPStatus
3+
14
from rest_framework import viewsets
5+
from rest_framework.decorators import action
6+
from rest_framework.exceptions import ValidationError
27
from rest_framework.permissions import IsAuthenticated
8+
from rest_framework.response import Response
39

10+
from api_app.analyzables_manager.filters import AnalyzableFilter
11+
from api_app.analyzables_manager.models import Analyzable
412
from api_app.analyzables_manager.serializers import AnalyzableSerializer
13+
from api_app.serializers.job import JobAnalyzableHistorySerializer
14+
from api_app.user_events_manager.serializers import (
15+
UserAnalyzableEventSerializer,
16+
UserDomainWildCardEventSerializer,
17+
UserIPWildCardEventSerializer,
18+
)
19+
20+
logger = logging.getLogger(__name__)
521

622

723
class AnalyzableViewSet(viewsets.ReadOnlyModelViewSet):
824

925
serializer_class = AnalyzableSerializer
1026
permission_classes = [IsAuthenticated]
27+
queryset = Analyzable.objects.all()
28+
filterset_class = AnalyzableFilter
1129

1230
def get_queryset(self):
1331
user = self.request.user
1432
return super().get_queryset().visible_for_user(user)
33+
34+
@action(detail=True)
35+
def history(self, request, pk=None):
36+
user = request.user
37+
try:
38+
analyzable: Analyzable = self.get_queryset().get(pk=pk)
39+
except Analyzable.DoesNotExist:
40+
raise ValidationError({"detail": "Requested analyzable does not exist."})
41+
42+
jobs_queryset = analyzable.jobs.visible_for_user(user).order_by(
43+
"-finished_analysis_time"
44+
)
45+
user_events_queryset = analyzable.user_events.visible_for_user(user).order_by(
46+
"-date"
47+
)
48+
user_domain_wildcard_events_queryset = (
49+
analyzable.user_domain_wildcard_events.visible_for_user(user).order_by(
50+
"-date"
51+
)
52+
)
53+
user_ip_wildcard_events_queryset = (
54+
analyzable.user_ip_wildcard_events.visible_for_user(user).order_by("-date")
55+
)
56+
57+
jobs = JobAnalyzableHistorySerializer(jobs_queryset, many=True).data
58+
user_events = UserAnalyzableEventSerializer(
59+
user_events_queryset, many=True
60+
).data
61+
user_domain_wildcard_events = UserDomainWildCardEventSerializer(
62+
user_domain_wildcard_events_queryset, many=True
63+
).data
64+
user_ip_wildcard_events = UserIPWildCardEventSerializer(
65+
user_ip_wildcard_events_queryset, many=True
66+
).data
67+
68+
return Response(
69+
status=HTTPStatus.OK.value,
70+
data={
71+
"jobs": jobs,
72+
"user_events": user_events,
73+
"user_domain_wildcard_events": user_domain_wildcard_events,
74+
"user_ip_wildcard_events": user_ip_wildcard_events,
75+
},
76+
)

api_app/choices.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,29 @@ def calculate_observable(cls, value: str) -> str:
161161

162162
return classification
163163

164+
@classmethod
165+
def get_data_model_class(cls, classification: str) -> typing.Type:
166+
from api_app.data_model_manager.models import (
167+
DomainDataModel,
168+
FileDataModel,
169+
IPDataModel,
170+
)
171+
172+
if classification == cls.IP.value:
173+
return IPDataModel
174+
elif classification in [
175+
cls.URL.value,
176+
cls.DOMAIN.value,
177+
]:
178+
return DomainDataModel
179+
elif classification in [
180+
cls.HASH.value,
181+
cls.FILE.value,
182+
]:
183+
return FileDataModel
184+
else:
185+
raise NotImplementedError()
186+
164187

165188
class ScanMode(models.IntegerChoices):
166189
FORCE_NEW_ANALYSIS = 1

api_app/serializers/job.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,3 +1288,24 @@ def to_representation(self, instance: Job):
12881288
@staticmethod
12891289
def get_playbook(instance: Job):
12901290
return instance.playbook_to_execute.name if instance.playbook_to_execute else ""
1291+
1292+
1293+
class JobAnalyzableHistorySerializer(rfs.ModelSerializer):
1294+
id = rfs.CharField(source="pk")
1295+
user = rfs.CharField(source="user.username", allow_null=False, read_only=True)
1296+
data_model = rfs.SerializerMethodField()
1297+
date = rfs.DateTimeField(
1298+
source="finished_analysis_time", read_only=True, allow_null=False
1299+
)
1300+
playbook = rfs.CharField(
1301+
source="playbook_to_execute.name", allow_null=True, read_only=True
1302+
)
1303+
1304+
class Meta:
1305+
model = Job
1306+
fields = ["playbook", "user", "date", "data_model", "id"]
1307+
1308+
def get_data_model(self, instance: Job):
1309+
if instance.data_model:
1310+
return instance.data_model.serialize()
1311+
return {}

api_app/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
path("", include("api_app.investigations_manager.urls")),
5959
path("data_model/", include("api_app.data_model_manager.urls")),
6060
path("user_event/", include("api_app.user_events_manager.urls")),
61+
path("", include("api_app.analyzables_manager.urls")),
6162
# auth
6263
path("auth/", include("authentication.urls")),
6364
# certego_saas:
Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,104 @@
1+
import ipaddress
2+
13
import rest_framework_filters as filters
4+
from django.contrib.contenttypes.models import ContentType
5+
6+
from api_app.data_model_manager.models import (
7+
DomainDataModel,
8+
FileDataModel,
9+
IPDataModel,
10+
)
11+
from api_app.user_events_manager.models import UserAnalyzableEvent, UserEvent
212

313

414
class UserEventFilterSet(filters.FilterSet):
515

616
username = filters.CharFilter(lookup_expr="iexact", field_name="user__username")
717
next_decay = filters.DateRangeFilter()
8-
date = filters.DateRangeFilter()
18+
id = filters.CharFilter(method="filter_for_id")
19+
20+
class Meta:
21+
model = UserEvent
22+
fields = {
23+
"date": ["lte", "gte"],
24+
}
25+
26+
@staticmethod
27+
def filter_for_id(queryset, value, _id, *args, **kwargs):
28+
try:
29+
int_id = int(_id)
30+
except ValueError:
31+
# this is to manage bad data as input
32+
return queryset
33+
else:
34+
return queryset.filter(id=int_id)
935

1036

1137
class UserAnalyzableEventFilterSet(UserEventFilterSet):
1238
analyzable_name = filters.CharFilter(
1339
field_name="analyzable__name", lookup_expr="icontains"
1440
)
41+
event_date__gte = filters.CharFilter(method="filter_for_event_date")
42+
event_date__lte = filters.CharFilter(method="filter_for_event_date")
43+
44+
@staticmethod
45+
def filter_for_event_date(queryset, value, _date, *args, **kwargs):
46+
date_filters = {}
47+
if value.endswith("__gte"):
48+
date_filters["date__gte"] = _date
49+
if value.endswith("__lte"):
50+
date_filters["date__lte"] = _date
51+
52+
data_model_map = {
53+
DomainDataModel: ContentType.objects.get_for_model(DomainDataModel),
54+
IPDataModel: ContentType.objects.get_for_model(IPDataModel),
55+
FileDataModel: ContentType.objects.get_for_model(FileDataModel),
56+
}
57+
58+
user_events_ids = []
59+
for model, content_type in data_model_map.items():
60+
data_model_ids = model.objects.filter(**date_filters).values_list(
61+
"id", flat=True
62+
)
63+
if data_model_ids:
64+
ids = UserAnalyzableEvent.objects.filter(
65+
data_model_content_type=content_type,
66+
data_model_object_id__in=data_model_ids,
67+
).values_list("id", flat=True)
68+
user_events_ids.extend(ids)
69+
70+
return queryset.filter(id__in=user_events_ids)
1571

1672

1773
class UserDomainWildCardEventFilterSet(UserEventFilterSet):
1874
query = filters.CharFilter(field_name="query", lookup_expr="icontains")
1975
analyzables = filters.BaseInFilter(field_name="analyzables__name")
76+
event_date__gte = filters.CharFilter(
77+
field_name="data_model__date", lookup_expr="gte"
78+
)
79+
event_date__lte = filters.CharFilter(
80+
field_name="data_model__date", lookup_expr="lte"
81+
)
2082

2183

2284
class UserIPWildCardEventFilterSet(UserEventFilterSet):
2385
ip = filters.CharFilter(method="filter_for_ip", lookup_expr="icontains")
86+
network = filters.CharFilter(method="filter_for_network")
2487
analyzables = filters.BaseInFilter(field_name="analyzables__name")
88+
event_date__gte = filters.CharFilter(
89+
field_name="data_model__date", lookup_expr="gte"
90+
)
91+
event_date__lte = filters.CharFilter(
92+
field_name="data_model__date", lookup_expr="lte"
93+
)
94+
95+
@staticmethod
96+
def filter_for_ip(queryset, value, _ip, *args, **kwargs):
97+
return queryset.filter(start_ip__lte=_ip, end_ip__gte=_ip)
2598

2699
@staticmethod
27-
def filter_for_ip(queryset, value, user, *args, **kwargs):
28-
return queryset.filter(start_ip__lte=value, end_ip__gte=value)
100+
def filter_for_network(queryset, value, _network, *args, **kwargs):
101+
network = ipaddress.IPv4Network(_network)
102+
start_ip = str(network[0])
103+
end_ip = str(network[-1])
104+
return queryset.filter(start_ip=start_ip, end_ip=end_ip)

0 commit comments

Comments
 (0)