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" 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") 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", diff --git a/ghostwriter/modules/shared.py b/ghostwriter/modules/shared.py new file mode 100644 index 000000000..e6514ee94 --- /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 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/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() 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/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 @@
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 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: diff --git a/ghostwriter/reporting/resources.py b/ghostwriter/reporting/resources.py index d149f1580..be44d0853 100644 --- a/ghostwriter/reporting/resources.py +++ b/ghostwriter/reporting/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.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", ) 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 @@