From d971e9de8b905fbe2282ac78111de000ef198b38 Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Fri, 21 Apr 2023 12:40:32 -0700 Subject: [PATCH 01/12] Adjusted new log toast message to include log ID --- ghostwriter/oplog/views.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ghostwriter/oplog/views.py b/ghostwriter/oplog/views.py index 3a2dfa13d..1c01c66ed 100644 --- a/ghostwriter/oplog/views.py +++ b/ghostwriter/oplog/views.py @@ -240,14 +240,12 @@ def form_valid(self, form): form.save() # Create new API key for this oplog try: - project = form.instance.project.id oplog_name = form.instance.name - api_key_name = oplog_name - api_key, key = APIKey.objects.create_key(name=api_key_name[:50]) + _, key = APIKey.objects.create_key(name=oplog_name[:50]) # Pass the API key via the messages framework messages.info( self.request, - f'The logging API key for project {project} and log "{api_key}" is: {key}\r\nPlease store it somewhere safe: you will not be able to see it again.', + f"The logging API key for your log (ID #{form.instance.id}) is: {key}\r\nPlease store it somewhere safe: you will not be able to see it again.", extra_tags="api-key no-toast", ) except Exception: From 51faf8cd0ce8bbaca7df945f6d2200a93b4cd59c Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Fri, 21 Apr 2023 12:41:24 -0700 Subject: [PATCH 02/12] Fixed search icon alignment --- ghostwriter/shepherd/templates/shepherd/server_search.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghostwriter/shepherd/templates/shepherd/server_search.html b/ghostwriter/shepherd/templates/shepherd/server_search.html index 5e6521b92..42941b426 100644 --- a/ghostwriter/shepherd/templates/shepherd/server_search.html +++ b/ghostwriter/shepherd/templates/shepherd/server_search.html @@ -16,7 +16,7 @@
- +
From b8d5668ff717b9a9b7aba1d86a05b3e016932801 Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Fri, 21 Apr 2023 12:42:11 -0700 Subject: [PATCH 03/12] Changed server search by hostname to be case insensitive --- ghostwriter/shepherd/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ghostwriter/shepherd/views.py b/ghostwriter/shepherd/views.py index aae9ed6db..b35c13dad 100644 --- a/ghostwriter/shepherd/views.py +++ b/ghostwriter/shepherd/views.py @@ -612,10 +612,10 @@ def infrastructure_search(request): if search_term: server_qs = StaticServer.objects.filter( - Q(ip_address__contains=search_term) | Q(name__contains=search_term) + Q(ip_address__contains=search_term) | Q(name__icontains=search_term) ) vps_qs = TransientServer.objects.select_related("project").filter( - Q(ip_address__contains=search_term) | Q(name__contains=search_term) + Q(ip_address__contains=search_term) | Q(name__icontains=search_term) ) aux_qs = AuxServerAddress.objects.select_related("static_server").filter( ip_address__contains=search_term From ee860cb191cf7126a9efa919488d23594245ef31 Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Fri, 21 Apr 2023 14:10:34 -0700 Subject: [PATCH 04/12] Set project automatically for new logs created from project dash --- ghostwriter/oplog/forms.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/ghostwriter/oplog/forms.py b/ghostwriter/oplog/forms.py index 88412364b..02e2f0807 100644 --- a/ghostwriter/oplog/forms.py +++ b/ghostwriter/oplog/forms.py @@ -28,24 +28,31 @@ class Meta: def __init__(self, project=None, *args, **kwargs): super().__init__(*args, **kwargs) - # If this is an update, mark project as read-only + # If this is an update, mark the project field as read-only instance = getattr(self, "instance", None) if instance and instance.pk: self.fields["project"].disabled = True + + for field in self.fields: + self.fields[field].widget.attrs["autocomplete"] = "off" self.fields["name"].widget.attrs["placeholder"] = "Descriptive Name for Identification" self.fields["name"].label = "Name for the Log" self.fields["name"].help_text = "Enter a name for this log that will help you identify it" - # Limit the list to just projects not marked as complete + # Limit the list to the selected project and disable the field if this is log created for a specific project self.project_instance = project - active_projects = Project.objects.filter(complete=False).order_by("-start_date") - if active_projects: - self.fields["project"].empty_label = "-- Select an Active Project --" - else: - self.fields["project"].empty_label = "-- No Active Projects --" - self.fields["project"].queryset = active_projects - for field in self.fields: - self.fields[field].widget.attrs["autocomplete"] = "off" + if project: + self.fields["project"].queryset = Project.objects.filter(pk=project.pk) + self.fields["project"].disabled = True + + # Limit the list to active projects if this is a new log made from the sidebar + if not project: + active_projects = Project.objects.filter(complete=False).order_by("-start_date") + if active_projects: + self.fields["project"].empty_label = "-- Select an Active Project --" + else: + self.fields["project"].empty_label = "-- No Active Projects --" + self.fields["project"].queryset = active_projects # Design form layout with Crispy's ``FormHelper`` self.helper = FormHelper() From d0d119e74fd556e61ea5a839abdc12202d9f9ffa Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Fri, 21 Apr 2023 14:13:34 -0700 Subject: [PATCH 05/12] Fixed sidebar search fields to use query parameters for related filters --- ghostwriter/templates/base_generic.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ghostwriter/templates/base_generic.html b/ghostwriter/templates/base_generic.html index c3b75e769..9ac23e8a6 100644 --- a/ghostwriter/templates/base_generic.html +++ b/ghostwriter/templates/base_generic.html @@ -185,7 +185,7 @@
    {% comment %} Search for Clients {% endcomment %}
    -
    {% comment %} Clients Submenu Items {% endcomment %} @@ -199,7 +199,7 @@
      {% comment %} Search for Findings {% endcomment %}
      -
      {% comment %} Findings Submenu Items {% endcomment %} @@ -287,7 +287,7 @@
        {% comment %} Search for Domains {% endcomment %}
        -
        {% comment %} Domains Submenu Items {% endcomment %} From 98abb11069f9f4eef4d94135e79098dd4bcbd4e6 Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Thu, 27 Apr 2023 15:52:01 -0700 Subject: [PATCH 06/12] Updated list of required headers for log imports --- ghostwriter/oplog/templates/oplog/oplog_import.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghostwriter/oplog/templates/oplog/oplog_import.html b/ghostwriter/oplog/templates/oplog/oplog_import.html index e2e773e10..d247b2cc9 100644 --- a/ghostwriter/oplog/templates/oplog/oplog_import.html +++ b/ghostwriter/oplog/templates/oplog/oplog_import.html @@ -39,7 +39,7 @@

        CSV Headers

        Your csv file must have these headers:

        start_date, end_date, source_ip, dest_ip, tool, user_context, command, description, output, - comments, operator_name + comments, operator_name, tags, oplog_id
From f03c28c88b4d5502f43a8c341d23fb6a4841111d Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Thu, 27 Apr 2023 15:52:40 -0700 Subject: [PATCH 07/12] Added support for importing and exporting tags --- ghostwriter/modules/shared.py | 45 ++++++++++++++++++++++++++++++ ghostwriter/oplog/admin.py | 1 + ghostwriter/oplog/resources.py | 16 +++++++++-- ghostwriter/reporting/resources.py | 12 ++++++-- ghostwriter/shepherd/resources.py | 28 +++++++++++++++---- 5 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 ghostwriter/modules/shared.py diff --git a/ghostwriter/modules/shared.py b/ghostwriter/modules/shared.py new file mode 100644 index 000000000..9d0319d9f --- /dev/null +++ b/ghostwriter/modules/shared.py @@ -0,0 +1,45 @@ +"""This contains shared model resources used by multiple applications.""" + +# 3rd Party Libraries +from import_export import resources, widgets +from import_export.fields import Field +from taggit.forms import TagField +from taggit.models import Tag + +# The following is based on the following StackOverflow answer: +# https://stackoverflow.com/questions/59582619/how-to-import-django-taggit-tag-in-django-import-export + + +class TagWidget(widgets.ManyToManyWidget): + def render(self, value, obj=None): + return self.separator.join([obj.name for obj in value.all()]) + + def clean(self, value, row=None, *args, **kwargs): + values = TagField().clean(value) + return [Tag.objects.get_or_create(name=tag)[0] for tag in values] + + +class TagFieldImport(Field): + def save(self, obj, data, is_m2m=False): + if not self.readonly: + attrs = self.attribute.split("__") + for attr in attrs[:-1]: + obj = getattr(obj, attr, None) + cleaned = self.clean(data) + if cleaned is not None or self.saves_null_values: + if not is_m2m: + setattr(obj, attrs[-1], cleaned) + else: + getattr(obj, attrs[-1]).set(cleaned, clear=True) + + +def taggit_before_import_row(row): + """Check if the ``tags`` field is empty and set it to a comma (nothing) if it is.""" + # The ``django-import-export`` app looks at the ``tags`` field to determine if the field can be null or blank + # and will throw an error if it is not. The field doesn't have Django's ``null`` attribute set to ``True`` + # so imports will fail. ``TaggableManager`` doesn't set that attribute so this is a workaround to set the field to a + # comma if it is null. A comma is effectively null for the purposes of this application. + if "tags" in row.keys(): + # A blank field may be blank ("") or ``None`` depending on the import file format. + if row["tags"] == "" or row["tags"] is None: + row["tags"] = "," diff --git a/ghostwriter/oplog/admin.py b/ghostwriter/oplog/admin.py index 8e5e6dff1..0932438d1 100644 --- a/ghostwriter/oplog/admin.py +++ b/ghostwriter/oplog/admin.py @@ -7,6 +7,7 @@ from import_export import resources from import_export.admin import ImportExportModelAdmin +# Ghostwriter Libraries from ghostwriter.oplog.models import Oplog, OplogEntry from ghostwriter.oplog.resources import OplogEntryResource diff --git a/ghostwriter/oplog/resources.py b/ghostwriter/oplog/resources.py index a00fbb26c..8b01699ae 100644 --- a/ghostwriter/oplog/resources.py +++ b/ghostwriter/oplog/resources.py @@ -5,13 +5,22 @@ # 3rd Party Libraries from import_export import resources +from taggit.models import Tag # Ghostwriter Libraries +from ghostwriter.modules.shared import TagFieldImport, TagWidget, taggit_before_import_row from ghostwriter.oplog.models import OplogEntry class OplogEntryResource(resources.ModelResource): + """ + Import and export for :model:`oplog.OplogEntry`. + """ + + tags = TagFieldImport(attribute="tags", column_name="tags", widget=TagWidget(Tag, separator=","), default="") + def before_import_row(self, row, **kwargs): + taggit_before_import_row(row) if "start_date" in row.keys(): try: timestamp = int(float(row["start_date"])) @@ -29,9 +38,9 @@ def before_import_row(self, row, **kwargs): class Meta: model = OplogEntry - skip_unchanged = True - exclude = ("id",) - import_id_fields = ( + skip_unchanged = False + export_order = ( + "id", "oplog_id", "start_date", "end_date", @@ -44,4 +53,5 @@ class Meta: "output", "comments", "operator_name", + "tags", ) diff --git a/ghostwriter/reporting/resources.py b/ghostwriter/reporting/resources.py index d149f1580..e40735266 100644 --- a/ghostwriter/reporting/resources.py +++ b/ghostwriter/reporting/resources.py @@ -1,11 +1,13 @@ """This contains all the ``import_export`` model resources used by the Reporting application.""" # 3rd Party Libraries -from import_export import resources +from import_export import resources, widgets from import_export.fields import Field from import_export.widgets import ForeignKeyWidget +from taggit.models import Tag # Ghostwriter Libraries +from ghostwriter.modules.shared import TagFieldImport, TagWidget, taggit_before_import_row from ghostwriter.reporting.models import Finding, FindingType, Severity @@ -24,10 +26,14 @@ class FindingResource(resources.ModelResource): column_name="finding_type", widget=ForeignKeyWidget(FindingType, "finding_type"), ) + tags = TagFieldImport(attribute="tags", column_name="tags", widget=TagWidget(Tag, separator=",")) + + def before_import_row(self, row, **kwargs): + taggit_before_import_row(row) class Meta: model = Finding - skip_unchanged = True + skip_unchanged = False fields = ( "id", "severity", @@ -41,6 +47,7 @@ class Meta: "network_detection_techniques", "references", "finding_guidance", + "tags", ) export_order = ( "id", @@ -55,4 +62,5 @@ class Meta: "network_detection_techniques", "references", "finding_guidance", + "tags", ) diff --git a/ghostwriter/shepherd/resources.py b/ghostwriter/shepherd/resources.py index 2f4b0fcc6..b6553d2c1 100644 --- a/ghostwriter/shepherd/resources.py +++ b/ghostwriter/shepherd/resources.py @@ -4,8 +4,10 @@ from import_export import resources from import_export.fields import Field from import_export.widgets import ForeignKeyWidget +from taggit.models import Tag # Ghostwriter Libraries +from ghostwriter.modules.shared import TagFieldImport, TagWidget, taggit_before_import_row from ghostwriter.shepherd.models import ( Domain, DomainStatus, @@ -37,25 +39,33 @@ class DomainResource(resources.ModelResource): column_name="whois_status", widget=ForeignKeyWidget(WhoisStatus, "whois_status"), ) + tags = TagFieldImport(attribute="tags", column_name="tags", widget=TagWidget(Tag, separator=",")) + + def before_import_row(self, row, **kwargs): + taggit_before_import_row(row) class Meta: model = Domain - skip_unchanged = True - exclude = ("last_used_by",) + skip_unchanged = False + exclude = ("last_used_by", "last_health_check", "vt_permalink") export_order = ( "id", "name", "domain_status", - "health_status", - "whois_status", + "registrar", + "auto_renew", "creation", "expiration", - "auto_renew", + "health_status", + "whois_status", "expired", "categorization", + "dns", + "reset_dns", "note", "burned_explanation", + "tags", ) @@ -74,10 +84,14 @@ class StaticServerResource(resources.ModelResource): column_name="server_provider", widget=ForeignKeyWidget(ServerProvider, "server_provider"), ) + tags = TagFieldImport(attribute="tags", column_name="tags", widget=TagWidget(Tag, separator=",")) + + def before_import_row(self, row, **kwargs): + taggit_before_import_row(row) class Meta: model = StaticServer - skip_unchanged = True + skip_unchanged = False exclude = ("last_used_by",) export_order = ( @@ -86,4 +100,6 @@ class Meta: "name", "server_status", "server_provider", + "note", + "tags", ) From a1ac3c7ee8b1c0c39bfd5b8f6950337d8ea9a9b8 Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Thu, 27 Apr 2023 16:29:24 -0700 Subject: [PATCH 08/12] Added `OplogSerializer` and logs and their entries to report data --- ghostwriter/modules/custom_serializers.py | 24 ++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/ghostwriter/modules/custom_serializers.py b/ghostwriter/modules/custom_serializers.py index 27205d9ba..ed4076ad0 100644 --- a/ghostwriter/modules/custom_serializers.py +++ b/ghostwriter/modules/custom_serializers.py @@ -3,13 +3,13 @@ # Standard Libraries from datetime import datetime -# 3rd Party Libraries -import pytz -from bs4 import BeautifulSoup - # Django Imports from django.conf import settings from django.utils import dateformat + +# 3rd Party Libraries +import pytz +from bs4 import BeautifulSoup from rest_framework import serializers from rest_framework.serializers import ( RelatedField, @@ -21,7 +21,7 @@ # Ghostwriter Libraries from ghostwriter.commandcenter.models import CompanyInformation -from ghostwriter.oplog.models import OplogEntry +from ghostwriter.oplog.models import Oplog, OplogEntry from ghostwriter.reporting.models import ( Evidence, Finding, @@ -680,6 +680,19 @@ class Meta: fields = "__all__" +class OplogSerializer(TaggitSerializer, CustomModelSerializer): + """Serialize :model:`oplog.Oplog` entries.""" + + entries = OplogEntrySerializer( + many=True, + exclude=["id", "oplog_id"], + ) + + class Meta: + model = Oplog + fields = "__all__" + + class ReportDataSerializer(CustomModelSerializer): """Serialize :model:`rolodex:Project` and all related entries.""" @@ -730,6 +743,7 @@ class ReportDataSerializer(CustomModelSerializer): "client", ] ) + logs = OplogSerializer(source="project.oplog_set", many=True, exclude=["id", "mute_notifications", "project"]) company = SerializerMethodField("get_company_info") tools = SerializerMethodField("get_tools") From 995bcecb12a1bc175fd31f460ddcd7fd2421f0fb Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Mon, 1 May 2023 09:50:29 -0700 Subject: [PATCH 09/12] Removed unnecessary import --- ghostwriter/modules/shared.py | 2 +- ghostwriter/reporting/resources.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ghostwriter/modules/shared.py b/ghostwriter/modules/shared.py index 9d0319d9f..e6514ee94 100644 --- a/ghostwriter/modules/shared.py +++ b/ghostwriter/modules/shared.py @@ -1,7 +1,7 @@ """This contains shared model resources used by multiple applications.""" # 3rd Party Libraries -from import_export import resources, widgets +from import_export import widgets from import_export.fields import Field from taggit.forms import TagField from taggit.models import Tag diff --git a/ghostwriter/reporting/resources.py b/ghostwriter/reporting/resources.py index e40735266..be44d0853 100644 --- a/ghostwriter/reporting/resources.py +++ b/ghostwriter/reporting/resources.py @@ -1,7 +1,7 @@ """This contains all the ``import_export`` model resources used by the Reporting application.""" # 3rd Party Libraries -from import_export import resources, widgets +from import_export import resources from import_export.fields import Field from import_export.widgets import ForeignKeyWidget from taggit.models import Tag From 717583652d844ffb0e01720ddc87d5b1cf59ad5f Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Mon, 1 May 2023 09:51:28 -0700 Subject: [PATCH 10/12] Added the `logs` key to the linter --- ghostwriter/modules/linting_utils.py | 68 ++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/ghostwriter/modules/linting_utils.py b/ghostwriter/modules/linting_utils.py index 8fc1875a2..dcb48123e 100644 --- a/ghostwriter/modules/linting_utils.py +++ b/ghostwriter/modules/linting_utils.py @@ -367,6 +367,74 @@ "doc_type": 2, "tags": ["tag1", "tag2", "tag3"], }, + "logs": [ + { + "entries": [ + { + "tags": ["tag1", "tag2", "tag3"], + "start_date": "2023-03-23T17:09:00Z", + "end_date": "2023-03-23T17:10:02Z", + "source_ip": "DEBIAN-DEV (192.168.85.132)", + "dest_ip": "", + "tool": "poseidon", + "user_context": "cmaddalena", + "command": "help", + "description": "", + "output": "", + "comments": "", + "operator_name": "mythic_admin", + }, + { + "tags": ["tag1", "tag2", "tag3"], + "start_date": "2023-03-20T21:32:31Z", + "end_date": "2023-03-20T21:32:31Z", + "source_ip": "DEBIAN-DEV (192.168.85.132)", + "dest_ip": "", + "tool": "poseidon", + "user_context": "cmaddalena", + "command": "help ", + "description": "", + "output": "", + "comments": "", + "operator_name": "mythic_admin", + }, + ], + "name": "SpecterOps Red Team Logs", + }, + { + "entries": [ + { + "tags": ["tag1", "tag2", "tag3"], + "start_date": "2023-03-23T17:09:00Z", + "end_date": "2023-03-23T17:10:02Z", + "source_ip": "DEBIAN-DEV (192.168.85.132)", + "dest_ip": "", + "tool": "poseidon", + "user_context": "cmaddalena", + "command": "help", + "description": "", + "output": "", + "comments": "", + "operator_name": "mythic_admin", + }, + { + "tags": ["tag1", "tag2", "tag3"], + "start_date": "2023-03-20T21:32:31Z", + "end_date": "2023-03-20T21:32:31Z", + "source_ip": "DEBIAN-DEV (192.168.85.132)", + "dest_ip": "", + "tool": "poseidon", + "user_context": "cmaddalena", + "command": "help ", + "description": "", + "output": "", + "comments": "", + "operator_name": "mythic_admin", + }, + ], + "name": "SpecterOps Red Team Log #2", + }, + ], "company": { "name": "SpecterOps", "twitter": "@specterops", From bc08ff59813c0d4e9036c16eafd4a687616bc95d Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Mon, 1 May 2023 09:51:49 -0700 Subject: [PATCH 11/12] Added case change tool to the WYISWYG editor --- ghostwriter/static/js/tinymce/config.js | 6 +- .../static/js/tinymce/plugins/case/LICENSE.md | 21 ++ .../static/js/tinymce/plugins/case/plugin.js | 227 ++++++++++++++++++ .../js/tinymce/plugins/case/plugin.min.js | 8 + 4 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 ghostwriter/static/js/tinymce/plugins/case/LICENSE.md create mode 100644 ghostwriter/static/js/tinymce/plugins/case/plugin.js create mode 100644 ghostwriter/static/js/tinymce/plugins/case/plugin.min.js diff --git a/ghostwriter/static/js/tinymce/config.js b/ghostwriter/static/js/tinymce/config.js index dda7b836e..c9f4c771a 100644 --- a/ghostwriter/static/js/tinymce/config.js +++ b/ghostwriter/static/js/tinymce/config.js @@ -38,12 +38,12 @@ var default_config = { edit: { title: 'Edit', items: 'undo redo | cut copy paste | selectall | searchreplace' }, view: { title: 'View', items: 'code | visualchars visualblocks | preview' }, insert: { title: 'Insert', items: 'evidenceUpload codesample link' }, - format: { title: 'Format', items: 'bold italic underline strikethrough superscript subscript codeformat | formats fontformats fontsizes align | forecolor | removeformat' }, + format: { title: 'Format', items: 'bold italic underline strikethrough superscript subscript codeformat case | formats fontformats fontsizes align | forecolor | removeformat' }, tools: { title: 'Tools', items: 'code wordcount' }, }, toolbar_mode: 'floating', - plugins: 'visualchars visualblocks save preview lists image hr autosave advlist code wordcount codesample searchreplace paste link', - toolbar: 'subscript superscript bold italic underline link blockquote | bullist numlist | codesample codeInline | evidenceUpload | removeformat save', + plugins: 'visualchars visualblocks save preview lists image hr autosave advlist code wordcount codesample searchreplace paste link case', + toolbar: 'subscript superscript bold italic underline link blockquote case | bullist numlist | codesample codeInline | evidenceUpload | removeformat save', contextmenu: 'bold italic link removeformat', paste_as_text: true, paste_data_images: false, diff --git a/ghostwriter/static/js/tinymce/plugins/case/LICENSE.md b/ghostwriter/static/js/tinymce/plugins/case/LICENSE.md new file mode 100644 index 000000000..a4d629309 --- /dev/null +++ b/ghostwriter/static/js/tinymce/plugins/case/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Melquisedeque Brito de Lima + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ghostwriter/static/js/tinymce/plugins/case/plugin.js b/ghostwriter/static/js/tinymce/plugins/case/plugin.js new file mode 100644 index 000000000..510730fb7 --- /dev/null +++ b/ghostwriter/static/js/tinymce/plugins/case/plugin.js @@ -0,0 +1,227 @@ +/** + * @copyright ©Melqui Brito. All rights reserved. + * @author Melqui Brito + * @version 1.2.0 (2020-03-08) + * @description Tinymce custom plugin for changing text case. + */ + +(function () { + tinymce.PluginManager.add('case', + function (editor) { + + const strings = { + TOOLNAME: 'Change Case', + LOWERCASE: 'lowercase', + UPPERCASE: 'UPPERCASE', + SENTENCECASE: 'Sentence case', + TITLECASE: 'Title Case' + }, defaultTitleCaseExeptions = [ + 'at', 'by', 'in', 'of', 'on', 'up', 'to', 'en', 're', 'vs', + 'but', 'off', 'out', 'via', 'bar', 'mid', 'per', 'pro', 'qua', 'til', + 'from', 'into', 'unto', 'with', 'amid', 'anit', 'atop', 'down', 'less', 'like', 'near', 'over', 'past', 'plus', 'sans', 'save', 'than', 'thru', 'till', 'upon', + 'for', 'and', 'nor', 'but', 'or', 'yet', 'so', 'an', 'a', 'some', 'the' + ], getParameterArray = function (param) { + let value = editor.getParam(param); + if (value) { + if (Array.isArray(value)) { + return value; + } else if (typeof value === "string") { + return value.replace(/(\s{1,})/g, "?").trim().split('?'); + } + } + if (param === 'title_case_minors') { + return defaultTitleCaseExeptions + } + return false + } + + var titleCaseExceptions = getParameterArray('title_case_minors'), + toInclude = getParameterArray('include_to_title_case_minors'), + toRuleOut = getParameterArray('rule_out_from_title_case_minors'); + if (toInclude) { + toInclude.forEach((el) => { + if (defaultTitleCaseExeptions.indexOf(el) === -1) { + defaultTitleCaseExeptions.push(el) + } + }) + } + if (toRuleOut) { + toRuleOut.forEach((el) => { + defaultTitleCaseExeptions = defaultTitleCaseExeptions.filter(minor => minor !== el) + }) + } + /* + * Appending new functions to String.prototype... + */ + String.prototype.toSentenceCase = function () { + return this.toLowerCase().replace(/(^\s*\w|[\.\!\?]\s*\w)/g, function (c) { + return c.toUpperCase() + }); + } + String.prototype.toTitleCase = function () { + let tt = (str) => { + let s = str.split('.'), w; + for (let i in s) { + if (!s.hasOwnProperty(i)) { + continue; + } + let w = s[i].split(' '), + j = 0; + + if (s[i].trim().replace(/(^\s+|\s+$)/g, "").length > 0) { + for (j; j < w.length; j++) { + let found = false; + for (let k = 0; k < w[j].length; k++) { + if (w[j][k].match(/([a-z'áàâãäéèêëíìîïóòôõöúùûü])/i)) { + w[j] = w[j][k].toUpperCase() + w[j].slice(k + 1); + found = true; + break; + } + } + if (found) { + break; + } + } + for (j; j < w.length; j++) { + if (titleCaseExceptions.indexOf(w[j]) === -1) { + for (let k = 0; k < w[j].length; k++) { + if (w[j][k].match(/([a-z'áàâãäéèêëíìîïóòôõöúùûü])/i)) { + w[j] = w[j][k].toUpperCase() + w[j].slice(k + 1); + break; + } + } + } + } + s[i] = w.join(' '); + } + } + return s.join('.'); + }; + return tt(this.toLowerCase()); + } + + String.prototype.apply = function (method) { + switch (method) { + case strings.LOWERCASE: + return this.toLowerCase(); + case strings.UPPERCASE: + return this.toUpperCase(); + case strings.SENTENCECASE: + return this.toSentenceCase(); + case strings.TITLECASE: + return this.toTitleCase(); + default: + return this; + } + } + + const handler = function (node, method, r) { + if (r.first && r.last) { + node.textContent = node.textContent.slice(0, r.startOffset) + node.textContent.slice(r.startOffset, r.endOffset).apply(method) + node.textContent.slice(r.endOffset); + } else if (r.first && !r.last) { + node.textContent = node.textContent.slice(0, r.startOffset) + node.textContent.slice(r.startOffset).apply(method); + } else if (!r.first && r.last) { + node.textContent = node.textContent.slice(0, r.endOffset).apply(method) + node.textContent.slice(r.endOffset); + } else { + node.textContent = node.textContent.apply(method); + } + } + + const apply = function (method) { + let rng = editor.selection.getRng(), + bm = editor.selection.getBookmark(2, true), + walker = new tinymce.dom.TreeWalker(rng.startContainer), + first = rng.startContainer, + last = rng.endContainer, + startOffset = rng.startOffset, + endOffset = rng.endOffset, + current = walker.current(); + + do { + if (current.nodeName === '#text') { + handler(current, method, { + first: current === first, + last: current === last, + startOffset: startOffset, + endOffset: endOffset + }); + } + if (current === last) { + break; + } + current = walker.next(); + } while (current); + editor.save(); + editor.isNotDirty = true; + editor.focus(); + editor.selection.moveToBookmark(bm); + } + + const getMenuItems = function () { + return [ + { + type: "menuitem", + text: strings.LOWERCASE, + //onAction: lowerCase() + onAction: () => apply(strings.LOWERCASE) + }, + { + type: "menuitem", + text: strings.UPPERCASE, + //onAction: upperCase() + onAction: () => apply(strings.UPPERCASE) + }, + { + type: "menuitem", + text: strings.SENTENCECASE, + //onAction: sentenceCase() + onAction: () => apply(strings.SENTENCECASE) + }, + { + type: "menuitem", + text: strings.TITLECASE, + //onAction: titleCase() + onAction: () => apply(strings.TITLECASE) + } + ] + } + + const getMenuButton = function () { + return { + icon: 'change-case', + tooltip: strings.TOOLNAME, + fetch: function (callback) { + const items = getMenuItems(); + callback(items); + } + } + } + + const getNestedMenuItem = function () { + return { + text: strings.TOOLNAME, + getSubmenuItems: () => { + return getMenuItems(); + } + } + } + + editor.ui.registry.addMenuButton('case', getMenuButton()); + editor.ui.registry.addNestedMenuItem('case', getNestedMenuItem()); + + editor.addCommand('mceLowerCase', () => apply(strings.LOWERCASE)); + editor.addCommand('mceUpperCase', () => apply(strings.UPPERCASE)); + editor.addCommand('mceSentenceCase', () => apply(strings.SENTENCECASE)); + editor.addCommand('mceTitleCase', () => apply(strings.TITLECASE)); + + return { + getMetadata: function () { + return { + name: "Case", + url: "https://github.com/melquibrito/Case-change-tinymce-plugin" + } + } + } + } + ); +})(); diff --git a/ghostwriter/static/js/tinymce/plugins/case/plugin.min.js b/ghostwriter/static/js/tinymce/plugins/case/plugin.min.js new file mode 100644 index 000000000..2703e35d5 --- /dev/null +++ b/ghostwriter/static/js/tinymce/plugins/case/plugin.min.js @@ -0,0 +1,8 @@ +/** + * @copyright ©Melqui Brito. All rights reserved. + * @author Melqui Brito + * @version 1.2.0 (2020-03-08) + * @description Tinymce custom plugin for changing text case. + */ + + !function(){tinymce.PluginManager.add("case",function(t){const e="Change Case",n="lowercase",o="UPPERCASE",s="Sentence case",r="Title Case",i=["at","by","in","of","on","up","to","en","re","vs","but","off","out","via","bar","mid","per","pro","qua","til","from","into","unto","with","amid","anit","atop","down","less","like","near","over","past","plus","sans","save","than","thru","till","upon","for","and","nor","but","or","yet","so","an","a","some","the"],a=function(e){let n=t.getParam(e);if(n){if(Array.isArray(n))return n;if("string"==typeof n)return n.replace(/(\s{1,})/g,"?").trim().split("?")}return"title_case_minors"===e&&i};var c=a("title_case_minors"),l=a("include_to_title_case_minors"),f=a("rule_out_from_title_case_minors");l&&l.forEach(t=>{-1===i.indexOf(t)&&i.push(t)}),f&&f.forEach(t=>{i=i.filter(e=>e!==t)}),String.prototype.toSentenceCase=function(){return this.toLowerCase().replace(/(^\s*\w|[\.\!\?]\s*\w)/g,function(t){return t.toUpperCase()})},String.prototype.toTitleCase=function(){return(t=>{let e=t.split(".");for(let t in e){if(e.hasOwnProperty(t)){let n=e[t].split(" "),o=0;if(e[t].trim().replace(/(^\s+|\s+$)/g,"").length>0){for(;om(n)},{type:"menuitem",text:o,onAction:()=>m(o)},{type:"menuitem",text:s,onAction:()=>m(s)},{type:"menuitem",text:r,onAction:()=>m(r)}]};return t.ui.registry.addMenuButton("case",{icon:"change-case",tooltip:e,fetch:function(t){t(p())}}),t.ui.registry.addNestedMenuItem("case",{text:e,getSubmenuItems:()=>p()}),t.addCommand("mceLowerCase",()=>m(n)),t.addCommand("mceUpperCase",()=>m(o)),t.addCommand("mceSentenceCase",()=>m(s)),t.addCommand("mceTitleCase",()=>m(r)),{getMetadata:function(){return{name:"Case",url:"https://github.com/melquibrito/Case-change-tinymce-plugin"}}}})}(); From ebed4017fe59c0797f44d5805830220a9c11da3c Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Mon, 1 May 2023 09:56:40 -0700 Subject: [PATCH 12/12] Updated for v3.2.7 --- CHANGELOG.md | 15 +++++++++++++++ VERSION | 4 ++-- config/settings/base.py | 4 ++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5c775548..205919b46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v3.2.7] - 1 May 2023 + +### Added + +* Added support for exporting and importing tags for the current import/export models (log entries, domains, servers, and findings) + +### Changed + +* The legacy REST API key notification for new activity logs now displays the log's ID to be used with the API and extensions like `mythic_sync` and `cobalt_sync` +* When creating a new activity log from the project dashboard, that project will now be automatically selected for the new log + +### Fixed + +* Fixed sidebar search boxes not working as intended following changes in v3.2.3 (Closes #294) + ## [v3.2.6] - 10 April 2023 ### Changed diff --git a/VERSION b/VERSION index b745a9c9b..8a01a8478 100644 --- a/VERSION +++ b/VERSION @@ -1,2 +1,2 @@ -v3.2.6 -10 April 2023 +v3.2.7 +1 May 2023 diff --git a/config/settings/base.py b/config/settings/base.py index 4ea32bb85..2722985dd 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -11,9 +11,9 @@ # 3rd Party Libraries import environ -__version__ = "3.2.6" +__version__ = "3.2.7" VERSION = __version__ -RELEASE_DATE = "10 April 2023" +RELEASE_DATE = "1 May 2023" ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent APPS_DIR = ROOT_DIR / "ghostwriter"