Skip to content

Commit

Permalink
Merge branch 'plugin-system' into 'main'
Browse files Browse the repository at this point in the history
Plugin system

See merge request reportcreator/reportcreator!737
  • Loading branch information
MWedl committed Oct 28, 2024
2 parents dc57bb3 + 0d959ac commit 46f059e
Show file tree
Hide file tree
Showing 215 changed files with 152,436 additions and 9,628 deletions.
33 changes: 32 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ WORKDIR /app/packages/markdown/
COPY packages/markdown/package.json packages/markdown/package-lock.json /app/packages/markdown/
RUN npm install

WORKDIR /app/packages/nuxt-base-layer/
COPY packages/nuxt-base-layer/package.json packages/nuxt-base-layer/package-lock.json /app/packages/nuxt-base-layer/
RUN npm install

WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json /app/frontend/
RUN npm install
Expand All @@ -34,6 +38,7 @@ RUN npm install
FROM --platform=$BUILDPLATFORM frontend-dev AS frontend-test
# Include source code
COPY packages/markdown/ /app/packages/markdown/
COPY packages/nuxt-base-layer/ /app/packages/nuxt-base-layer/
COPY frontend /app/frontend/
COPY api/src/reportcreator_api/tasks/rendering/global_assets /app/frontend/src/assets/rendering/
COPY --from=pdfviewer /app/packages/pdfviewer/dist/ /app/frontend/src/public/static/pdfviewer/
Expand Down Expand Up @@ -74,6 +79,28 @@ RUN npm run build



FROM --platform=$BUILDPLATFORM frontend-test AS plugin-builder

WORKDIR /app/packages/plugin-base-layer/
COPY packages/plugin-base-layer /app/packages/plugin-base-layer/
RUN npm install

RUN apk add --no-cache \
bash \
git \
curl \
wget \
unzip \
jq

COPY plugins /app/plugins/
WORKDIR /app/plugins/
RUN /app/plugins/build.sh





FROM python:3.12-slim-bookworm AS api-dev

# Get a list a preinstalled apt packages
Expand Down Expand Up @@ -150,7 +177,8 @@ ENV VERSION=dev \
DEBUG=off \
MEDIA_ROOT=/data/ \
SERVER_WORKERS=4 \
PDF_RENDER_SCRIPT_PATH=/app/rendering/dist/bundle.js
PDF_RENDER_SCRIPT_PATH=/app/rendering/dist/bundle.js \
PLUGIN_DIRS=/app/plugins/

# Start server
EXPOSE 8000
Expand All @@ -169,6 +197,7 @@ COPY --chown=user:user rendering/dist /app/rendering/dist/
FROM --platform=$BUILDPLATFORM api-dev AS api-test
# Copy source code
COPY --chown=user:user api/src /app/api/
COPY --chown=user:user plugins /app/plugins/

# Copy generated template rendering script
COPY --from=rendering --chown=user:user /app/rendering/dist /app/rendering/dist/
Expand All @@ -184,11 +213,13 @@ RUN mv /app/api/frontend/static/index.html /app/api/frontend/index.html \
&& python3 manage.py collectstatic --no-input --no-post-process \
&& python3 -m whitenoise.compress /app/api/static/ map

COPY --from=plugin-builder --chown=user:user /app/plugins/ /app/plugins/


FROM api-test AS api
COPY --from=api-statics /app/api/frontend/index.html /app/api/frontend/index.html
COPY --from=api-statics /app/api/static/ /app/api/static/
COPY --from=api-statics /app/plugins/ /app/plugins/
USER 0
COPY --chown=1000:1000 api/generate_notice.sh api/download_sources.sh api/start.sh api/NOTICE /app/api/
RUN /bin/bash /app/api/generate_notice.sh
Expand Down
1 change: 1 addition & 0 deletions api/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,4 @@ src/frontend/*/static/*
!src/frontend/index.html
!src/frontend/static
!src/frontend/static/.gitkeep
src/sysreptor_plugins
24 changes: 20 additions & 4 deletions api/src/reportcreator_api/api_utils/backup_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
from django.core import serializers
from django.core.management import call_command
from django.core.management.color import no_style
from django.core.serializers.base import DeserializationError
from django.core.serializers.json import DjangoJSONEncoder
from django.db import connection, transaction
from django.db.migrations.executor import MigrationExecutor
from django.db.migrations.loader import MigrationLoader
from django.db.migrations.recorder import MigrationRecorder

from reportcreator_api.api_utils.models import BackupLog, BackupLogType
from reportcreator_api.archive import crypto
Expand Down Expand Up @@ -199,14 +199,20 @@ def destroy_database():
"""
Delete all DB data; drop all tables, views, sequences
"""
tables = connection.introspection.django_table_names(include_views=True) + [MigrationRecorder.Migration._meta.db_table]
tables = connection.introspection.table_names(include_views=False)
views = set(connection.introspection.table_names(include_views=True)) - set(tables)
connection.check_constraints()
with connection.cursor() as cursor:
cursor.execute(
'DROP TABLE IF EXISTS ' +
', '.join([connection.ops.quote_name(t) for t in tables]) +
' CASCADE;',
)
cursor.execute(
'DROP VIEW IF EXISTS ' +
', '.join([connection.ops.quote_name(v) for v in views]) +
' CASCADE;',
)


def restore_database_dump(f):
Expand All @@ -219,13 +225,19 @@ def restore_database_dump(f):
migration_apps = MigrationExecutor(connection)._create_project_state(with_applied_migrations=True).apps
def get_model(model_identifier):
app_label, model_name = model_identifier.split('.')
return migration_apps.get_model(app_label, model_name)
try:
return migration_apps.get_model(app_label, model_name)
except LookupError as ex:
if app_label.startswith('plugin_'):
raise DeserializationError(f'Plugin model "{model_identifier}" not found. Plugin is probably not enabled.') from ex
else:
raise ex

# Defer DB constraint checking
with constraint_checks_disabled(), \
mock.patch('django.core.serializers.python._get_model', get_model):
objs_with_deferred_fields = []
for obj in serializers.deserialize('jsonl', f, handle_forward_references=True):
for obj in serializers.deserialize('jsonl', f, handle_forward_references=True, ignorenonexistent=True):
obj.save()
if obj.deferred_fields:
objs_with_deferred_fields.append(obj)
Expand Down Expand Up @@ -326,6 +338,10 @@ def restore_backup(z, keepfiles=True):
logging.info('Begin running migrations from backup')
if migrations is not None:
for m in migrations:
if m['app_label'].startswith('plugin_') and not any(a for a in apps.get_app_configs() if a.label == m['app_label']):
logging.warning(f'Cannot run migation "{m["migration_name"]}", because plugin "{m["app_label"]}" is not enabled. Plugin data will not be restored.')
continue

call_command('migrate', app_label=m['app_label'], migration_name=m['migration_name'], interactive=False, verbosity=0)
else:
logging.warning('No migrations info found in backup. Applying all available migrations')
Expand Down
26 changes: 24 additions & 2 deletions api/src/reportcreator_api/api_utils/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.conf import settings
from django.utils import timezone
from drf_spectacular.utils import OpenApiTypes, extend_schema
from rest_framework import routers, viewsets
from rest_framework import routers, views, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.serializers import Serializer
Expand All @@ -24,6 +24,7 @@
LanguageToolAddWordSerializer,
LanguageToolSerializer,
)
from reportcreator_api.conf import plugins
from reportcreator_api.pentests.customfields.types import CweField
from reportcreator_api.pentests.models import Language, ProjectMemberRole
from reportcreator_api.tasks.models import PeriodicTask
Expand Down Expand Up @@ -153,7 +154,7 @@ def list(self, *args, **kwargs):

@extend_schema(responses=OpenApiTypes.OBJECT)
@action(detail=False, url_name='settings', url_path='settings')
def settings_endpoint(self, *args, **kwargs):
def settings_endpoint(self, request, *args, **kwargs):
languages = [{
'code': l.value,
'name': l.label,
Expand Down Expand Up @@ -193,6 +194,14 @@ def settings_endpoint(self, *args, **kwargs):
'share_notes': settings.GUEST_USERS_CAN_SHARE_NOTES,
'see_all_users': settings.GUEST_USERS_CAN_SEE_ALL_USERS,
},
'plugins': [
{
'id': p.plugin_id,
'name': p.name.split('.')[-1],
'frontend_entry': p.get_frontend_entry(request),
'frontend_settings': p.get_frontend_settings(request),
} for p in plugins.enabled_plugins
],
})


Expand All @@ -212,3 +221,16 @@ async def get(self, request, *args, **kwargs):
gc.collect()

return res


class PluginApiView(views.APIView):
schema = None

def get(self, request, *args, **kwargs):
out = {}
for p in plugins.enabled_plugins:
plugin_api_root = None
if p.urlpatterns:
plugin_api_root = request.build_absolute_uri(f'/api/plugins/{p.plugin_id}/api/')
out[p.name.split('.')[-1]] = plugin_api_root
return Response(data=out)
Loading

0 comments on commit 46f059e

Please sign in to comment.