Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Analysis framework #2114

Merged
merged 27 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions api_app/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ class JobAdminView(CustomAdminView):
)
list_filter = ("status", "user", "tags")

def has_add_permission(self, request: HttpRequest) -> bool:
return False

def has_change_permission(self, request: HttpRequest, obj=None) -> bool:
return False

@admin.display(description="Tags")
def get_tags(self, instance: Job):
return [tag.label for tag in instance.tags.all()]
Expand Down Expand Up @@ -144,6 +150,9 @@ class AbstractReportAdminView(CustomAdminView):
def has_add_permission(request):
return False

def has_change_permission(self, request: HttpRequest, obj=None) -> bool:
return False


@admin.register(Parameter)
class ParameterAdminView(CustomAdminView):
Expand Down
Empty file.
27 changes: 27 additions & 0 deletions api_app/analyses_manager/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from django.contrib import admin

from api_app.admin import CustomAdminView
from api_app.analyses_manager.models import Analysis


@admin.register(Analysis)
class AnalysisAdminView(CustomAdminView):
list_display = ["name", "start_time", "status", "owner", "get_jobs", "total_jobs"]
list_filter = ["owner", "status"]
search_fields = ["name"]

@admin.display(description="Total Jobs")
def total_jobs(self, instance: Analysis):
from api_app.models import Job

string = ""
for i, job in enumerate(instance.jobs.all()):
job: Job
tree = job.get_tree(job)
jobs_repr = " ".join(map(str, tree.values_list("pk", flat=True)))
string += f"Branch {i+1}: jobs -> {jobs_repr}; "
return string

@admin.display(description="Jobs at first level")
def get_jobs(self, instance: Analysis):
return list(instance.jobs.all().values_list("pk", flat=True))
12 changes: 12 additions & 0 deletions api_app/analyses_manager/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl
# See the file 'LICENSE' for copying permission.

from django.apps import AppConfig


class AnalysesManagerConfig(AppConfig):
name = "api_app.analyses_manager"

@staticmethod
def ready() -> None:
from . import signals # noqa
7 changes: 7 additions & 0 deletions api_app/analyses_manager/choices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.db import models


class AnalysisStatusChoices(models.TextChoices):
CREATED = "created"
RUNNING = "running"
CONCLUDED = "concluded"
64 changes: 64 additions & 0 deletions api_app/analyses_manager/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Generated by Django 4.2.8 on 2024-02-01 14:27

import datetime

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="Analysis",
options={"verbose_name_plural": "analyses"},
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("for_organization", models.BooleanField(default=False)),
("name", models.CharField(max_length=100)),
("description", models.TextField(blank=True, default="")),
("start_time", models.DateTimeField(default=datetime.datetime.now)),
("end_time", models.DateTimeField(blank=True, default=None, null=True)),
(
"status",
models.CharField(
choices=[
("created", "Created"),
("running", "Running"),
("concluded", "Concluded"),
],
default="started",
max_length=20,
),
),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="analyses",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.AddIndex(
model_name="analysis",
index=models.Index(
fields=["start_time"], name="analyses_ma_start_t_6a1f30_idx"
),
),
]
Empty file.
87 changes: 87 additions & 0 deletions api_app/analyses_manager/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from datetime import datetime
from typing import List

from django.conf import settings
from django.db import models

from api_app.analyses_manager.choices import AnalysisStatusChoices
from api_app.analyses_manager.queryset import AnalysisQuerySet
from api_app.choices import TLP
from api_app.interfaces import OwnershipAbstractModel


class Analysis(OwnershipAbstractModel):
name = models.CharField(max_length=100)
description = models.TextField(default="", blank=True)

start_time = models.DateTimeField(default=datetime.now)
end_time = models.DateTimeField(default=None, null=True, blank=True)
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="analyses",
)
status = models.CharField(
choices=AnalysisStatusChoices.choices,
max_length=20,
default=AnalysisStatusChoices.CREATED.value,
)
Status = AnalysisStatusChoices

objects = AnalysisQuerySet.as_manager()

class Meta:
verbose_name_plural = "analyses"
indexes = [models.Index(fields=["start_time"])]

def __str__(self):
return (
f"{self.name}:"
f" jobs {', '.join([str(job.pk) for job in self.jobs.all()])} "
f"-> {self.status}"
)

def set_correct_status(self, save: bool = True):
from api_app.models import Job

# if I have some jobs
if self.jobs.exists():
# and at least one is running
if self.jobs.exclude(status__in=Job.Status.final_statuses()).count() > 0:
self.status = self.Status.RUNNING.value
self.end_time = None
# and they are all completed
else:
self.status = self.Status.CONCLUDED.value
self.end_time = (
self.jobs.order_by("-finished_analysis_time")
.first()
.finished_analysis_time
)
else:
self.status = self.Status.CREATED.value
self.end_time = None
if save:
self.save(update_fields=["status", "end_time"])

@property
def tags(self) -> List[str]:
return list(set(self.jobs.values_list("tags__label", flat=True)))

@property
def tlp(self) -> TLP:
return (
max(
TLP[tlp_string]
for tlp_string in self.jobs.values_list("tlp", flat=True)
)
if self.jobs.exists()
else TLP.CLEAR.value
)

@property
def total_jobs(self) -> int:
return (
sum(job.get_descendant_count() for job in self.jobs.all())
+ self.jobs.count()
)
5 changes: 5 additions & 0 deletions api_app/analyses_manager/queryset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from api_app.queryset import CleanOnCreateQuerySet, ModelWithOwnershipQuerySet


class AnalysisQuerySet(CleanOnCreateQuerySet, ModelWithOwnershipQuerySet):
...
30 changes: 30 additions & 0 deletions api_app/analyses_manager/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl
# See the file 'LICENSE' for copying permission.
from rest_framework import serializers as rfs

from api_app.analyses_manager.models import Analysis
from api_app.serializers import ModelWithOwnershipSerializer
from api_app.serializers.job import JobTreeSerializer


class AnalysisSerializer(ModelWithOwnershipSerializer, rfs.ModelSerializer):
tags = rfs.ListField(
child=rfs.CharField(read_only=True), read_only=True, default=[]
)
tlp = rfs.CharField(read_only=True)
total_jobs = rfs.IntegerField(read_only=True)
jobs = rfs.PrimaryKeyRelatedField(many=True, read_only=True)
status = rfs.CharField(read_only=True)
owner = rfs.HiddenField(default=rfs.CurrentUserDefault())

class Meta:
model = Analysis
fields = rfs.ALL_FIELDS


class AnalysisTreeSerializer(rfs.ModelSerializer):
class Meta:
model = Analysis
fields = ["name", "owner", "jobs"]

jobs = JobTreeSerializer(many=True)
Empty file.
11 changes: 11 additions & 0 deletions api_app/analyses_manager/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.urls import include, path
from rest_framework import routers

from .views import AnalysisViewSet

router = routers.DefaultRouter(trailing_slash=False)
router.register(r"analysis", AnalysisViewSet, basename="analysis")

urlpatterns = [
path(r"", include(router.urls)),
]
108 changes: 108 additions & 0 deletions api_app/analyses_manager/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl
# See the file 'LICENSE' for copying permission.
import logging

from django.core.exceptions import BadRequest
from django.http import HttpRequest
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

from ..mixins import PaginationMixin
from ..models import Job
from ..permissions import IsObjectOwnerOrSameOrgPermission
from ..views import ModelWithOwnershipViewSet
from .models import Analysis
from .serializers import AnalysisSerializer, AnalysisTreeSerializer

logger = logging.getLogger(__name__)


class AnalysisViewSet(PaginationMixin, ModelWithOwnershipViewSet, ModelViewSet):
permission_classes = [IsAuthenticated, IsObjectOwnerOrSameOrgPermission]
serializer_class = AnalysisSerializer
ordering = ["name"]
queryset = Analysis.objects.all()

def get_queryset(self):
return super().get_queryset().prefetch_related("jobs")

def get_object(self):
obj = super().get_object()
if not obj.for_organization and obj.owner != self.request.user:
raise PermissionDenied("You can't use other people private analyses")
return obj

def _get_job(self, request):
try:
job_pk = request.POST["job"]
except KeyError:
raise BadRequest("You should set the `job` argument in the data")
try:
job = Job.objects.get(pk=job_pk)
except Job.DoesNotExist:
raise BadRequest(f"Job {job_pk} does not exist")
return job

def _check_job_and_analysis(self, job, analysis):
if (
# same organization if analysis is at org level
analysis.for_organization
and (
job.user.has_membership()
and analysis.owner.has_membership()
and job.user.organization == analysis.owner.organization
)
# or same user
) or job.user == analysis.owner:
return True
raise PermissionDenied(
"You do not have permissions to add this job to the analysis"
)

@action(methods=["POST"], url_name="add_job", detail=True)
def add_job(self, request, pk):
analysis: Analysis = self.get_object()
job: Job = self._get_job(request)
self._check_job_and_analysis(job, analysis)
if job.analysis is None:
job.analysis = analysis
job.save()
# we are possibly changing the status of the analysis
job.analysis.set_correct_status(save=True)

return Response(
status=status.HTTP_200_OK,
data=AnalysisSerializer(instance=analysis).data,
)

elif job.analysis_id == analysis.id:
raise BadRequest("Job is already part of this analysis")
else:
raise BadRequest("Job is already part of different analysis")

@action(methods=["POST"], url_name="remove_job", detail=True)
def remove_job(self, request, pk):
analysis: Analysis = self.get_object()
request: HttpRequest
job: Job = self._get_job(request)
self._check_job_and_analysis(job, analysis)
if job.analysis_id != analysis.pk:
raise BadRequest(f"You can't remove job {job.id} from analysis")
job.analysis = None
job.save()
# we are possibly changing the status of the analysis
analysis.set_correct_status(save=True)
return Response(
status=status.HTTP_200_OK, data=AnalysisSerializer(instance=analysis).data
)

@action(methods=["GET"], url_name="graph", detail=True)
def tree(self, request, pk):
obj: Analysis = self.get_object()
return Response(
status=status.HTTP_200_OK, data=AnalysisTreeSerializer(instance=obj).data
)
Loading
Loading