Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
8 changes: 8 additions & 0 deletions api_app/analyzers_manager/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,11 @@ class AllTypes(models.TextChoices):
HASH = "hash"
GENERIC = "generic"
FILE = "file"


class HTTPMethods(models.TextChoices):
GET = "get"
POST = "post"
PUT = "put"
PATCH = "patch"
DELETE = "delete"
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from django.db import migrations


def migrate_python_module_pivot(apps, schema_editor):
PythonModule = apps.get_model("api_app", "PythonModule")
pm, _ = PythonModule.objects.update_or_create(
module="basic_observable_analyzer.BasicObservableAnalyzer",
base_path="api_app.analyzers_manager.observable_analyzers",
)
Parameter = apps.get_model("api_app", "Parameter")
Parameter.objects.get_or_create(
name="url",
type="str",
python_module=pm,
is_secret=False,
required=True,
defaults={
"description": "URL of the instance you want to connect to",
},
)
Parameter.objects.get_or_create(
name="api_key_name",
type="str",
python_module=pm,
is_secret=True,
required=False,
defaults={
"description": "API key required for authentication",
},
)
Parameter.objects.get_or_create(
name="headers",
type="dict",
python_module=pm,
is_secret=False,
required=False,
defaults={
"description": "Headers used for the request",
},
)
Parameter.objects.get_or_create(
name="http_method",
type="str",
python_module=pm,
is_secret=False,
required=True,
defaults={
"description": "HTTP method used for the request",
},
)
Parameter.objects.get_or_create(
name="params",
type="dict",
python_module=pm,
is_secret=False,
required=False,
defaults={
"description": "Params used for the query string or request payload",
},
)
Parameter.objects.get_or_create(
name="certificate",
type="str",
python_module=pm,
is_secret=True,
required=False,
defaults={
"description": "Instance SSL certificate (multiline string).",
},
)


def reverse_migrate_module_pivot(apps, schema_editor):
PythonModule = apps.get_model("api_app", "PythonModule")
PythonModule.objects.get(
module="basic_observable_analyzer.BasicObservableAnalyzer",
base_path="api_app.analyzers_manager.observable_analyzers",
).delete()


class Migration(migrations.Migration):
dependencies = [
("analyzers_manager", "0122_alter_soft_time_limit"),
]
operations = [
migrations.RunPython(migrate_python_module_pivot, reverse_migrate_module_pivot)
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import base64
import logging
from tempfile import NamedTemporaryFile

import requests

from api_app.analyzers_manager.classes import ObservableAnalyzer
from api_app.analyzers_manager.constants import HTTPMethods
from api_app.analyzers_manager.exceptions import (
AnalyzerConfigurationException,
AnalyzerRunException,
)
from tests.mock_utils import MockUpResponse, if_mock_connections, patch

logger = logging.getLogger(__name__)


class BasicObservableAnalyzer(ObservableAnalyzer):
url: str
headers: dict
params: dict
_certificate: str
_api_key_name: str
http_method: str = "get"

@staticmethod
def _clean_certificate(cert):
return (
cert.replace("-----BEGIN CERTIFICATE-----", "-----BEGIN_CERTIFICATE-----")
.replace("-----END CERTIFICATE-----", "-----END_CERTIFICATE-----")
.replace(" ", "\n")
.replace("-----BEGIN_CERTIFICATE-----", "-----BEGIN CERTIFICATE-----")
.replace("-----END_CERTIFICATE-----", "-----END CERTIFICATE-----")
)

def update(self) -> bool:
pass

def run(self):
# optional authentication
if hasattr(self, "_api_key_name") and "Authorization" in self.headers.keys():
api_key = self._api_key_name
auth_schema = self.headers["Authorization"].split(" ")[0]
if auth_schema == "Basic":
# the API uses basic auth so we need to base64 encode the auth payload
api_key = base64.b64encode(self._api_key_name.encode()).decode()
self.headers["Authorization"] = self.headers["Authorization"].replace(
"<api_key>", api_key
)
elif hasattr(self, "_api_key_name"):
for key in self.headers.keys():
self.headers[key] = self.headers[key].replace(
"<api_key>", self._api_key_name
)

# optional certificate
verify = True # defualt
if hasattr(self, "_certificate"):
self.__cert_file = NamedTemporaryFile(mode="w")
self.__cert_file.write(self._clean_certificate(self._certificate))
self.__cert_file.flush()
verify = self.__cert_file.name

# replace <observable> placheholder
if hasattr(self, "params"):
for key in self.params.keys():
if self.params[key] == "<observable>":
self.params[key] = self.observable_name

# validate url
if not hasattr(self, "url"):
raise AnalyzerConfigurationException("Instance URL is required")

# request
if self.http_method not in HTTPMethods.values:
raise AnalyzerConfigurationException("Http method is not valid")

try:
if self.http_method == HTTPMethods.GET:
if hasattr(self, "params"):
response = requests.get(
self.url,
params=self.params,
headers=self.headers,
verify=verify,
)
else:
response = requests.get(
self.url + self.observable_name,
headers=self.headers,
verify=verify,
)
else:
request_method = getattr(requests, self.http_method)
response = request_method(
self.url, headers=self.headers, json=self.params, verify=verify
)
response.raise_for_status()
except requests.RequestException as e:
raise AnalyzerRunException(e)

response_json = response.json()
logger.debug(f"response received: {response_json}")
return response_json

@classmethod
def _monkeypatch(cls):
patches = [
if_mock_connections(
patch(
"requests.get",
return_value=MockUpResponse({}, 200),
),
)
]
return super()._monkeypatch(patches=patches)
43 changes: 43 additions & 0 deletions api_app/analyzers_manager/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# 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 ..models import PluginConfig, PythonModule
from ..serializers.plugin import (
PluginConfigSerializer,
PythonConfigSerializer,
PythonConfigSerializerForMigration,
)
Expand All @@ -23,11 +27,50 @@ class Meta:


class AnalyzerConfigSerializer(PythonConfigSerializer):
plugin_config = rfs.ListField(
child=rfs.DictField(), write_only=True, required=False
)
python_module = rfs.SlugRelatedField(
queryset=PythonModule.objects.all(), slug_field="module"
)

class Meta:
model = AnalyzerConfig
exclude = PythonConfigSerializer.Meta.exclude
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
for config in plugin_config:
plugin_config_serializer = PluginConfigSerializer(
data=config, context={"request": self.context["request"]}
)
plugin_config_serializer.is_valid(raise_exception=True)
plugin_config_serializer.save()
return pc

def update(self, instance, validated_data):
plugin_config = validated_data.pop("plugin_config", [])
pc = super().update(instance, validated_data)

# update plugin config
for config in plugin_config:
plugin_config_serializer = PluginConfigSerializer(
data=config, context={"request": self.context["request"]}
)
plugin_config_serializer.is_valid(raise_exception=True)
PluginConfig.objects.filter(
owner=self.context["request"].user,
analyzer_config=plugin_config_serializer.validated_data[
"analyzer_config"
],
parameter=plugin_config_serializer.validated_data["parameter"],
).update_or_create(plugin_config_serializer.validated_data)
return pc


class AnalyzerConfigSerializerForMigration(PythonConfigSerializerForMigration):
class Meta:
Expand Down
19 changes: 17 additions & 2 deletions api_app/analyzers_manager/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
# See the file 'LICENSE' for copying permission.
import logging

from rest_framework import mixins

from ..permissions import isPluginActionsPermission
from ..views import PythonConfigViewSet, PythonReportActionViewSet
from .filters import AnalyzerConfigFilter
from .models import AnalyzerReport
from .models import AnalyzerConfig, AnalyzerReport
from .serializers import AnalyzerConfigSerializer

logger = logging.getLogger(__name__)
Expand All @@ -16,9 +19,21 @@
]


class AnalyzerConfigViewSet(PythonConfigViewSet):
class AnalyzerConfigViewSet(
PythonConfigViewSet,
mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
):
serializer_class = AnalyzerConfigSerializer
filterset_class = AnalyzerConfigFilter
queryset = AnalyzerConfig.objects.all()

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


class AnalyzerActionViewSet(PythonReportActionViewSet):
Expand Down
10 changes: 10 additions & 0 deletions api_app/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,13 @@ def has_object_permission(request, view, obj):
and obj_owner.membership.organization
== request.user.membership.organization
)


class isPluginActionsPermission(BasePermission):
@staticmethod
def has_object_permission(request, view, obj):
# only an admin or superuser can update or delete plugins
if request.user.has_membership():
return request.user.membership.is_admin
else:
return request.user.is_superuser
29 changes: 25 additions & 4 deletions api_app/pivots_manager/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from api_app.analyzers_manager.models import AnalyzerConfig
from api_app.connectors_manager.models import ConnectorConfig
from api_app.models import Job, PythonModule
from api_app.models import Job, PluginConfig, 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 (
Expand Down Expand Up @@ -77,7 +77,9 @@ class PivotConfigSerializer(PythonConfigSerializer):
python_module = rfs.SlugRelatedField(
queryset=PythonModule.objects.all(), slug_field="module"
)
plugin_config = rfs.DictField(write_only=True, required=False)
plugin_config = rfs.ListField(
child=rfs.DictField(), write_only=True, required=False
)

class Meta:
model = PivotConfig
Expand All @@ -102,14 +104,33 @@ def create(self, validated_data):
pc = super().create(validated_data)

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

def update(self, instance, validated_data):
plugin_config = validated_data.pop("plugin_config", [])
pc = super().update(instance, validated_data)

# update plugin config
for config in plugin_config:
plugin_config_serializer = PluginConfigSerializer(
data=config, context={"request": self.context["request"]}
)
plugin_config_serializer.is_valid(raise_exception=True)
PluginConfig.objects.filter(
owner=self.context["request"].user,
analyzer_config=plugin_config_serializer.validated_data[
"analyzer_config"
],
parameter=plugin_config_serializer.validated_data["parameter"],
).update_or_create(plugin_config_serializer.validated_data)
return pc


class PivotConfigSerializerForMigration(PythonConfigSerializerForMigration):
related_analyzer_configs = rfs.SlugRelatedField(
Expand Down
1 change: 0 additions & 1 deletion api_app/serializers/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,6 @@ class PythonConfigSerializer(AbstractConfigSerializer):

class Meta:
exclude = [
"python_module",
"routing_key",
"soft_time_limit",
"health_check_status",
Expand Down
Loading