Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
10 changes: 10 additions & 0 deletions api_app/pivots_manager/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,13 @@ def has_object_permission(request, view, obj):
obj.starting_job.user.pk == request.user.pk
and obj.ending_job.user.pk == request.user.pk
)


class PivotActionsPermission(BasePermission):
@staticmethod
def has_object_permission(request, view, obj):
# only an admin or superuser can update or delete pivots
if request.user.has_membership():
return request.user.membership.is_admin
else:
return request.user.is_superuser
23 changes: 19 additions & 4 deletions api_app/pivots_manager/serializers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from rest_framework import serializers as rfs
from rest_framework.exceptions import ValidationError

from api_app.models import Job
from api_app.models import Job, PythonModule
from api_app.pivots_manager.models import PivotConfig, PivotMap, PivotReport
from api_app.playbooks_manager.models import PlaybookConfig
from api_app.serializers.plugin import (
PluginConfigSerializer,
PythonConfigSerializer,
PythonConfigSerializerForMigration,
)
Expand Down Expand Up @@ -56,16 +57,30 @@ class PivotConfigSerializer(PythonConfigSerializer):
playbooks_choice = rfs.SlugRelatedField(
queryset=PlaybookConfig.objects.all(), slug_field="name", many=True
)

name = rfs.CharField(read_only=True)
description = rfs.CharField(read_only=True)
related_configs = rfs.SlugRelatedField(read_only=True, many=True, slug_field="name")
python_module = rfs.SlugRelatedField(
queryset=PythonModule.objects.all(), slug_field="module"
)
plugin_config = rfs.DictField(write_only=True, required=False)

class Meta:
model = PivotConfig
exclude = ["related_analyzer_configs", "related_connector_configs"]
list_serializer_class = PythonConfigSerializer.Meta.list_serializer_class

def create(self, validated_data):
plugin_config = validated_data.pop("plugin_config", {})
pc = super().create(validated_data)

# create plugin config
if plugin_config:
plugin_config_serializer = PluginConfigSerializer(
data=plugin_config, context={"request": self.context["request"]}
)
plugin_config_serializer.is_valid(raise_exception=True)
plugin_config_serializer.save()
return pc


class PivotConfigSerializerForMigration(PythonConfigSerializerForMigration):
related_analyzer_configs = rfs.SlugRelatedField(
Expand Down
2 changes: 1 addition & 1 deletion api_app/pivots_manager/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def pre_save_pivot_config(
sender, instance: PivotConfig, raw, using, update_fields, *args, **kwargs
):
try:
if instance.pk:
if instance.pk and not instance.description:
instance.description = instance._generate_full_description()
else:
instance.description = (
Expand Down
20 changes: 17 additions & 3 deletions api_app/pivots_manager/views.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
from rest_framework import viewsets
from rest_framework import mixins, viewsets
from rest_framework.permissions import IsAuthenticated

from api_app.pivots_manager.models import PivotMap, PivotReport
from api_app.pivots_manager.permissions import PivotOwnerPermission
from api_app.pivots_manager.permissions import (
PivotActionsPermission,
PivotOwnerPermission,
)
from api_app.pivots_manager.serializers import PivotConfigSerializer, PivotMapSerializer
from api_app.views import PythonConfigViewSet, PythonReportActionViewSet


class PivotConfigViewSet(PythonConfigViewSet):
class PivotConfigViewSet(
PythonConfigViewSet,
mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
):
serializer_class = PivotConfigSerializer

def get_permissions(self):
permissions = super().get_permissions()
if self.action in ["destroy", "update", "partial_update"]:
permissions.append(PivotActionsPermission())
return permissions


class PivotActionViewSet(PythonReportActionViewSet):
@classmethod
Expand Down
18 changes: 12 additions & 6 deletions api_app/playbooks_manager/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from api_app.serializers import ModelWithOwnershipSerializer
from api_app.serializers.job import TagSerializer
from api_app.serializers.plugin import AbstractConfigSerializerForMigration
from api_app.visualizers_manager.models import VisualizerConfig


class PlaybookConfigSerializerForMigration(AbstractConfigSerializerForMigration):
Expand All @@ -31,7 +32,7 @@ class Meta:
model = PlaybookConfig
fields = rfs.ALL_FIELDS

type = rfs.ListField(child=rfs.CharField(read_only=True), read_only=True)
type = rfs.ListField(child=rfs.CharField(), required=False)
analyzers = rfs.SlugRelatedField(
many=True,
queryset=AnalyzerConfig.objects.all(),
Expand All @@ -48,27 +49,32 @@ class Meta:
pivots = rfs.SlugRelatedField(
many=True, queryset=PivotConfig.objects.all(), required=True, slug_field="name"
)
visualizers = rfs.SlugRelatedField(read_only=True, many=True, slug_field="name")
visualizers = rfs.SlugRelatedField(
many=True,
queryset=VisualizerConfig.objects.all(),
required=False,
slug_field="name",
)

runtime_configuration = rfs.DictField(required=True)

scan_mode = rfs.ChoiceField(choices=ScanMode.choices, required=True)
scan_check_time = DayDurationField(required=True, allow_null=True)
tags = TagSerializer(required=False, allow_empty=True, many=True, read_only=True)
tlp = rfs.CharField(read_only=True)
tlp = rfs.CharField()
weight = rfs.IntegerField(read_only=True, required=False, allow_null=True)
is_deletable = rfs.SerializerMethodField()
is_editable = rfs.SerializerMethodField()
tags_labels = rfs.ListField(
child=rfs.CharField(required=True),
default=list,
required=False,
write_only=True,
)

def get_is_deletable(self, instance: PlaybookConfig):
def get_is_editable(self, instance: PlaybookConfig):
# if the playbook is not a default one
if instance.owner:
# it is deletable by the owner of the playbook
# it is editable/deletable by the owner of the playbook
# or by an admin of the same organization
if instance.owner == self.context["request"].user or (
self.context["request"].user.membership.is_admin
Expand Down
10 changes: 6 additions & 4 deletions api_app/playbooks_manager/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from typing import Type

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from rest_framework.exceptions import ValidationError

from api_app.pivots_manager.models import PivotConfig
from api_app.playbooks_manager.models import PlaybookConfig
Expand Down Expand Up @@ -75,7 +75,9 @@ def m2m_changed_pivots_playbook_config(
wrong_pivots = objects.exclude(pk__in=valid_pks)
if wrong_pivots.exists():
raise ValidationError(
f"You can't set pivot{'s' if wrong_pivots.size()> 0 else ''}"
f" {','.join(wrong_pivots.values_list('name', flat=True))} because"
" the playbook does not have all the required plugins"
{
"detail": "You can't set pivots"
f" {','.join(wrong_pivots.values_list('name', flat=True))} because"
" the playbook does not have all the required plugins"
}
)
10 changes: 5 additions & 5 deletions api_app/serializers/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from api_app.connectors_manager.models import ConnectorConfig
from api_app.ingestors_manager.models import IngestorConfig
from api_app.models import Parameter, PluginConfig, PythonConfig, PythonModule
from api_app.pivots_manager.models import PivotConfig
from api_app.serializers import ModelWithOwnershipSerializer
from api_app.serializers.celery import CrontabScheduleSerializer
from api_app.visualizers_manager.models import VisualizerConfig
Expand Down Expand Up @@ -77,7 +78,7 @@ def to_representation(self, value):
return json.dumps(result)
return result

type = rfs.ChoiceField(choices=["1", "2", "3", "4"]) # retrocompatibility
type = rfs.ChoiceField(choices=["1", "2", "3", "4", "5"]) # retrocompatibility
config_type = rfs.ChoiceField(choices=["1", "2"]) # retrocompatibility
attribute = rfs.CharField()
plugin_name = rfs.CharField()
Expand Down Expand Up @@ -110,6 +111,8 @@ def validate(self, attrs):
class_ = VisualizerConfig
elif _type == "4":
class_ = IngestorConfig
elif _type == "5":
class_ = PivotConfig
else:
raise RuntimeError("Not configured")
# we set the pointers allowing retro-compatibility from the frontend
Expand Down Expand Up @@ -262,7 +265,7 @@ class AbstractConfigSerializer(rfs.ModelSerializer):


class PythonConfigSerializer(AbstractConfigSerializer):
parameters = ParameterSerializer(write_only=True, many=True)
parameters = ParameterSerializer(write_only=True, many=True, required=False)

class Meta:
exclude = [
Expand All @@ -274,9 +277,6 @@ class Meta:
]
list_serializer_class = PythonConfigListSerializer

def to_internal_value(self, data):
raise NotImplementedError()

def to_representation(self, instance: PythonConfig):
result = super().to_representation(instance)
result["disabled"] = result["disabled"] | instance.health_check_status
Expand Down
89 changes: 89 additions & 0 deletions frontend/src/components/common/form/ScanConfigSelectInput.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from "react";
import PropTypes from "prop-types";
import { FormGroup, Input, Label, UncontrolledTooltip } from "reactstrap";
import { MdInfoOutline } from "react-icons/md";

import { ScanModesNumeric } from "../../../constants/advancedSettingsConst";

export function ScanConfigSelectInput(props) {
const { formik } = props;
console.debug("ScanConfigSelectInput - formik:");
console.debug(formik);

return (
<div>
<FormGroup
check
key="checkchoice__check_all"
className="d-flex align-items-center justify-content-between"
>
<div>
<Input
id="checkchoice__check_all"
type="radio"
name="scan_mode"
value={ScanModesNumeric.CHECK_PREVIOUS_ANALYSIS}
onChange={formik.handleChange}
checked={
formik.values.scan_mode ===
ScanModesNumeric.CHECK_PREVIOUS_ANALYSIS
}
/>
<Label check for="checkchoice__check_all">
Do not execute if a similar analysis is currently running or
reported without fails
</Label>
</div>
<div className="col-3 d-flex align-items-center">
H:
<div className="col-8 mx-1">
<Input
id="checkchoice__check_all__minutes_ago"
type="number"
name="scan_check_time"
value={formik.values.scan_check_time}
onChange={formik.handleChange}
/>
</div>
<div className="col-2">
<MdInfoOutline id="minutes-ago-info-icon" />
<UncontrolledTooltip
target="minutes-ago-info-icon"
placement="right"
fade={false}
innerClassName="p-2 border border-info text-start text-nowrap md-fit-content"
>
<span>
Max age (in hours) for the similar analysis.
<br />
The default value is 24 hours (1 day).
<br />
Empty value takes all the previous analysis.
</span>
</UncontrolledTooltip>
</div>
</div>
</FormGroup>

<FormGroup check key="checkchoice__force_new">
<Input
id="checkchoice__force_new"
type="radio"
name="scan_mode"
value={ScanModesNumeric.FORCE_NEW_ANALYSIS}
onChange={formik.handleChange}
checked={
formik.values.scan_mode === ScanModesNumeric.FORCE_NEW_ANALYSIS
}
/>
<Label check for="checkchoice__force_new">
Force new analysis
</Label>
</FormGroup>
</div>
);
}

ScanConfigSelectInput.propTypes = {
formik: PropTypes.object.isRequired,
};
Loading