Skip to content

Commit 635e827

Browse files
authored
Merge pull request #148 from intelowlproject/develop
Elastic Search, LDAP, groups/permissions, various other improvements.
2 parents d54dc98 + 9764f90 commit 635e827

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+738
-257
lines changed

.dockerignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ __pycache__
55
env_file_app
66
env_file_postgres
77
env_file_integrations
8-
venv/
8+
venv/
9+
settings/ldap_config.py
10+
docker-compose-override.yml

.flake8

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ exclude =
88
docker-compose*,
99
venv,
1010
migrations,
11-
virtualenv
11+
virtualenv,
12+
ldap_config.py

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ env_file_postgres
77
env_file_integrations
88
.env
99
venv/
10+
settings/ldap_config.py
11+
docker-compose-override.yml

.travis.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ branches:
44
- develop
55
sudo: required
66
language: python
7+
cache: pip
78
dist: bionic
89
python:
910
- '3.6'
@@ -18,4 +19,6 @@ install:
1819
script:
1920
- sudo docker exec -ti intel_owl_uwsgi black . --check --exclude "migrations|venv"
2021
- sudo docker exec -ti intel_owl_uwsgi flake8 . --count
21-
- sudo docker exec -ti intel_owl_uwsgi python manage.py test tests
22+
- sudo docker exec -ti intel_owl_uwsgi python manage.py test tests
23+
after_success:
24+
- bash <(curl -s https://codecov.io/bash)

Dockerfile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ ENV PYTHONUNBUFFERED 1
44
ENV DJANGO_SETTINGS_MODULE intel_owl.settings
55
ENV PYTHONPATH /opt/deploy/intel_owl
66
ENV LOG_PATH /var/log/intel_owl
7+
ENV ELASTICSEARCH_DSL_VERSION 7.1.4
78

89
RUN mkdir -p ${LOG_PATH} \
910
${LOG_PATH}/django ${LOG_PATH}/uwsgi \
@@ -13,20 +14,23 @@ RUN mkdir -p ${LOG_PATH} \
1314

1415
RUN apt-get update \
1516
&& apt-get install -y --no-install-recommends apt-utils libsasl2-dev libssl-dev \
16-
vim libfuzzy-dev net-tools python-psycopg2 git osslsigncode exiftool \
17+
vim libldap2-dev python-dev libfuzzy-dev net-tools python-psycopg2 git osslsigncode exiftool \
1718
&& apt-get clean \
1819
&& rm -rf /var/lib/apt/lists/*
1920
RUN pip3 install --upgrade pip
2021

2122
COPY requirements.txt $PYTHONPATH/requirements.txt
2223
WORKDIR $PYTHONPATH
2324

24-
RUN pip3 install --compile -r requirements.txt
25+
RUN pip3 install --no-cache-dir --compile -r requirements.txt
26+
# install elasticsearch-dsl's appropriate version as specified by user
27+
RUN pip3 install --no-cache-dir django-elasticsearch-dsl==${ELASTICSEARCH_DSL_VERSION}
2528

2629
COPY . $PYTHONPATH
2730

2831
RUN touch ${LOG_PATH}/django/api_app.log ${LOG_PATH}/django/api_app_errors.log \
2932
&& touch ${LOG_PATH}/django/celery.log ${LOG_PATH}/django/celery_errors.log \
33+
&& touch ${LOG_PATH}/django/django_auth_ldap.log \
3034
&& chown -R www-data:www-data ${LOG_PATH} /opt/deploy/ \
3135
# this is cause stringstifer creates this directory during the build and cause celery to crash
3236
&& rm -rf /root/.local

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,12 @@ Documentation about IntelOwl installation, usage, contribution can be found at h
4646
- Static RTF Analysis
4747
- Static PDF Analysis
4848
- Static PE Analysis
49+
- Static APK Analysis
4950
- Static Generic File Analysis
5051
- Strings analysis
5152
- PE Signature verification
53+
- PE Capabilities Extraction
54+
- Emulated Javascript Analysis
5255

5356
**Free modules that require additional configuration**:
5457

@@ -125,7 +128,10 @@ license terms.
125128
[Yara community rules](https://github.com/Yara-Rules),
126129
[Neo23x0 Yara sigs](https://github.com/Neo23x0/signature-base),
127130
[Intezer Yara sigs](https://github.com/intezer/yara-rules),
128-
[McAfee Yara sigs](https://github.com/advanced-threat-research/Yara-Rules)
131+
[McAfee Yara sigs](https://github.com/advanced-threat-research/Yara-Rules),
132+
[APKiD](https://github.com/rednaga/APKiD/blob/master/LICENSE.COMMERCIAL),
133+
[Box-JS](https://github.com/CapacitorSet/box-js/blob/master/LICENSE),
134+
[Capa](https://github.com/fireeye/capa/blob/master/LICENSE.txt)
129135

130136
### Acknowledgments
131137

@@ -147,4 +153,4 @@ Feel free to contact the author at any time:
147153
Matteo Lodi ([Twitter](https://twitter.com/matte_lodi))
148154

149155

150-
We also have a dedicated twitter account for the project: [@intel_owl](https://twitter.com/intel_owl).
156+
We also have a dedicated twitter account for the project: [@intel_owl](https://twitter.com/intel_owl).

api_app/admin.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,28 @@
44
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken
55
from rest_framework_simplejwt.tokens import RefreshToken
66
from rest_framework_simplejwt.utils import datetime_from_epoch
7+
from guardian.admin import GuardedModelAdmin
78

89
from .models import Job, Tag
910
from intel_owl.settings import CLIENT_TOKEN_LIFETIME_DAYS, SIMPLE_JWT as jwt_settings
1011

1112

12-
class JobAdminView(admin.ModelAdmin):
13+
class JobAdminView(GuardedModelAdmin):
1314
list_display = (
1415
"id",
16+
"status",
1517
"source",
1618
"observable_name",
17-
"status",
1819
"observable_classification",
20+
"file_name",
1921
"file_mimetype",
2022
"received_request_time",
2123
)
2224
list_display_link = ("id", "status")
2325
search_fields = ("source", "md5", "observable_name")
2426

2527

26-
class TagAdminView(admin.ModelAdmin):
28+
class TagAdminView(GuardedModelAdmin):
2729
list_display = ("id", "label", "color")
2830
search_fields = ("label", "color")
2931

api_app/api.py

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22

33
from api_app import models, serializers, helpers
4+
from api_app.permissions import ExtendedObjectPermissions
45
from .script_analyzers import general
56

67
from wsgiref.util import FileWrapper
@@ -10,6 +11,9 @@
1011
from rest_framework.response import Response
1112
from rest_framework import status, viewsets
1213
from rest_framework.decorators import api_view
14+
from rest_framework.permissions import DjangoObjectPermissions
15+
from guardian.decorators import permission_required_or_403
16+
from rest_framework_guardian.filters import ObjectPermissionsFilter
1317

1418

1519
logger = logging.getLogger(__name__)
@@ -19,6 +23,7 @@
1923

2024

2125
@api_view(["GET"])
26+
@permission_required_or_403("api_app.view_job")
2227
def ask_analysis_availability(request):
2328
"""
2429
This is useful to avoid repeating the same analysis multiple times.
@@ -115,12 +120,13 @@ def ask_analysis_availability(request):
115120
except Exception as e:
116121
logger.exception(f"ask_analysis_availability requester:{source} error:{e}.")
117122
return Response(
118-
{"error": "error in ask_analysis_availability. Check logs."},
123+
{"detail": "error in ask_analysis_availability. Check logs."},
119124
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
120125
)
121126

122127

123128
@api_view(["POST"])
129+
@permission_required_or_403("api_app.add_job")
124130
def send_analysis_request(request):
125131
"""
126132
This endpoint allows to start a Job related to a file or an observable
@@ -145,6 +151,9 @@ def send_analysis_request(request):
145151
list of id's of tags to apply to job
146152
:param [run_all_available_analyzers]: bool
147153
default False
154+
:param [private]: bool
155+
default False,
156+
enable it to allow view permissions to only requesting user's groups.
148157
:param [force_privacy]: bool
149158
default False,
150159
enable it if you want to avoid to run analyzers with privacy issues
@@ -172,7 +181,9 @@ def send_analysis_request(request):
172181

173182
params = {"source": source}
174183

175-
serializer = serializers.JobSerializer(data=data_received)
184+
serializer = serializers.JobSerializer(
185+
data=data_received, context={"request": request}
186+
)
176187
if serializer.is_valid():
177188
serialized_data = serializer.validated_data
178189
logger.info(f"serialized_data: {serialized_data}")
@@ -234,20 +245,20 @@ def send_analysis_request(request):
234245

235246
# save the arrived data plus new params into a new job object
236247
serializer.save(**params)
237-
job_id = serializer.data.get("id", "")
248+
job_id = serializer.data.get("id", None)
238249
md5 = serializer.data.get("md5", "")
239-
logger.info(f"new job_id {job_id} for md5 {md5}")
250+
logger.info(f"New Job added with ID: #{job_id} and md5: {md5}.")
240251
if not job_id:
241252
return Response({"error": "815"}, status=status.HTTP_400_BAD_REQUEST)
242253

243254
else:
244255
error_message = f"serializer validation failed: {serializer.errors}"
245-
logger.info(error_message)
256+
logger.error(error_message)
246257
return Response(
247258
{"error": error_message}, status=status.HTTP_400_BAD_REQUEST
248259
)
249260

250-
is_sample = serializer.data.get("is_sample", "")
261+
is_sample = serializer.data.get("is_sample", False)
251262
if not test:
252263
general.start_analyzers(
253264
params["analyzers_to_execute"], analyzers_config, job_id, md5, is_sample
@@ -267,12 +278,13 @@ def send_analysis_request(request):
267278
except Exception as e:
268279
logger.exception(f"receive_analysis_request requester:{source} error:{e}.")
269280
return Response(
270-
{"error": "error in send_analysis_request. Check logs"},
281+
{"detail": "error in send_analysis_request. Check logs"},
271282
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
272283
)
273284

274285

275286
@api_view(["GET"])
287+
@permission_required_or_403("api_app.view_job")
276288
def ask_analysis_result(request):
277289
"""
278290
Endpoint to retrieve the status and results of a specific Job based on its ID
@@ -299,6 +311,12 @@ def ask_analysis_result(request):
299311
job_id = data_received["job_id"]
300312
try:
301313
job = models.Job.objects.get(id=job_id)
314+
# check permission
315+
if not request.user.has_perm("api_app.view_job", job):
316+
return Response(
317+
{"detail": "You don't have permission to perform this operation."},
318+
status=status.HTTP_403_FORBIDDEN,
319+
)
302320
except models.Job.DoesNotExist:
303321
response_dict = {"status": "not_available"}
304322
else:
@@ -308,7 +326,7 @@ def ask_analysis_result(request):
308326
"job_id": str(job.id),
309327
}
310328
# adding elapsed time
311-
finished_analysis_time = getattr(job, "finished_analysis_time", "")
329+
finished_analysis_time = getattr(job, "finished_analysis_time", None)
312330
if not finished_analysis_time:
313331
finished_analysis_time = helpers.get_now()
314332
elapsed_time = finished_analysis_time - job.received_request_time
@@ -360,32 +378,37 @@ def download_sample(request):
360378
"""
361379
this method is used to download a sample from a Job ID
362380
:param request: job_id
363-
:return 200 found, 404 not found
381+
:returns: 200 if found, 404 not found, 403 forbidden
364382
"""
365383
try:
366384
data_received = request.query_params
367385
logger.info(f"Get binary by Job ID. Data received {data_received}")
368386
if "job_id" not in data_received:
369387
return Response({"error": "821"}, status=status.HTTP_400_BAD_REQUEST)
388+
# get job object
370389
try:
371390
job = models.Job.objects.get(id=data_received["job_id"])
372391
except models.Job.DoesNotExist:
373-
return Response({"answer": "not found"}, status=status.HTTP_200_OK)
392+
return Response({"detail": "not found"}, status=status.HTTP_404_NOT_FOUND)
393+
# check permission
394+
if not request.user.has_perm("api_app.view_job", job):
395+
return Response(
396+
{"detail": "You don't have permission to perform this operation."},
397+
status=status.HTTP_403_FORBIDDEN,
398+
)
399+
# make sure it is a sample
374400
if not job.is_sample:
375401
return Response(
376-
{"answer": "job without sample"}, status=status.HTTP_400_BAD_REQUEST
402+
{"detail": "job without sample"}, status=status.HTTP_400_BAD_REQUEST
377403
)
378-
file_mimetype = job.file_mimetype
379-
response = HttpResponse(FileWrapper(job.file), content_type=file_mimetype)
380-
response["Content-Disposition"] = "attachment; filename={}".format(
381-
job.file_name
382-
)
404+
response = HttpResponse(FileWrapper(job.file), content_type=job.file_mimetype)
405+
response["Content-Disposition"] = f"attachment; filename={job.file_name}"
383406
return response
384407

385408
except Exception as e:
386409
logger.exception(f"download_sample requester:{str(request.user)} error:{e}.")
387410
return Response(
388-
{"error": "error in download_sample. Check logs."},
411+
{"detail": "error in download_sample. Check logs."},
389412
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
390413
)
391414

@@ -406,23 +429,31 @@ class JobViewSet(viewsets.ReadOnlyModelViewSet):
406429
if wrong HTTP method
407430
"""
408431

409-
queryset = models.Job.objects.all()
432+
queryset = models.Job.objects.order_by("-received_request_time").all()
410433
serializer_class = serializers.JobSerializer
411-
412-
def list(self, request):
413-
queryset = (
414-
models.Job.objects.order_by("-received_request_time")
415-
.defer("analysis_reports", "errors")
416-
.all()
417-
)
418-
serializer = serializers.JobListSerializer(queryset, many=True)
419-
return Response(serializer.data)
434+
serializer_action_classes = {
435+
"list": serializers.JobListSerializer,
436+
}
437+
permission_classes = (ExtendedObjectPermissions,)
438+
filter_backends = (ObjectPermissionsFilter,)
439+
440+
def get_serializer_class(self, *args, **kwargs):
441+
"""
442+
Instantiate the list of serializers per action from class attribute
443+
(must be defined).
444+
"""
445+
kwargs["partial"] = True
446+
try:
447+
return self.serializer_action_classes[self.action]
448+
except (KeyError, AttributeError):
449+
return super(JobViewSet, self).get_serializer_class()
420450

421451

422452
class TagViewSet(viewsets.ModelViewSet):
423453
"""
424454
REST endpoint to pefrom CRUD operations on Job tags.
425455
Requires authentication.
456+
POST/PUT/DELETE requires model/object level permission.
426457
427458
:methods_allowed:
428459
GET, POST, PUT, DELETE, OPTIONS
@@ -437,3 +468,4 @@ class TagViewSet(viewsets.ModelViewSet):
437468

438469
queryset = models.Tag.objects.all()
439470
serializer_class = serializers.TagSerializer
471+
permission_classes = (DjangoObjectPermissions,)

api_app/apps.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33

44
class ApiAppConfig(AppConfig):
55
name = "api_app"
6+
7+
def ready(self):
8+
# flake8: noqa
9+
import api_app.signal_handlers as signals

0 commit comments

Comments
 (0)