From 20702d645efcacec32a3629c330c263d64e63a7b Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Thu, 18 Jul 2024 13:19:37 -0700 Subject: [PATCH 1/5] Fixed error with report generation when a cloud server lacked a name Added `or asset['ip_address']` to use the IP address when the object does not have a `name` or `domain` value. --- .../modules/reportwriter/project/base.py | 88 ++++++++++++------- 1 file changed, 56 insertions(+), 32 deletions(-) diff --git a/ghostwriter/modules/reportwriter/project/base.py b/ghostwriter/modules/reportwriter/project/base.py index edf7396bd..d32028999 100644 --- a/ghostwriter/modules/reportwriter/project/base.py +++ b/ghostwriter/modules/reportwriter/project/base.py @@ -1,4 +1,3 @@ - from collections import ChainMap import copy @@ -26,10 +25,7 @@ def serialize_object(self, object): def map_rich_texts(self): base_context = copy.deepcopy(self.data) - rich_text_context = ChainMap( - ExportProjectBase.rich_text_jinja_overlay(self.data), - base_context - ) + rich_text_context = ChainMap(ExportProjectBase.rich_text_jinja_overlay(self.data), base_context) # Fields on Project ExportProjectBase.process_projects_richtext(self, base_context, rich_text_context) @@ -80,54 +76,74 @@ def process_projects_richtext( f"client {base_context['client']['name']}", base_context["client"]["extra_fields"], Client, - rich_text_context + rich_text_context, ) # Project - base_context["project"]["note_rt"] = ex.create_lazy_template("the project note", base_context["project"]["note"], rich_text_context) + base_context["project"]["note_rt"] = ex.create_lazy_template( + "the project note", base_context["project"]["note"], rich_text_context + ) ex.process_extra_fields("the project", base_context["project"]["extra_fields"], Project, rich_text_context) # Assignments for assignment in base_context["team"]: if isinstance(assignment, dict): if assignment["note"]: - assignment["note_rt"] = ex.create_lazy_template(f"the note of person {assignment['name']}", assignment["note"], rich_text_context) + assignment["note_rt"] = ex.create_lazy_template( + f"the note of person {assignment['name']}", assignment["note"], rich_text_context + ) # Contacts for contact in base_context["client"]["contacts"]: if isinstance(contact, dict): if contact["note"]: - contact["note_rt"] = ex.create_lazy_template(f"the note of contact {contact['name']}", contact["note"], rich_text_context) + contact["note_rt"] = ex.create_lazy_template( + f"the note of contact {contact['name']}", contact["note"], rich_text_context + ) # Objectives for objective in base_context["objectives"]: if isinstance(objective, dict): if objective["description"]: - objective["description_rt"] = ex.create_lazy_template(f"the description of objective {objective['objective']}", objective["description"], rich_text_context) + objective["description_rt"] = ex.create_lazy_template( + f"the description of objective {objective['objective']}", + objective["description"], + rich_text_context, + ) # Scope Lists for scope_list in base_context["scope"]: if isinstance(scope_list, dict): if scope_list["description"]: - scope_list["description_rt"] = ex.create_lazy_template(f"the description of scope {scope_list['name']}", scope_list["description"], rich_text_context) + scope_list["description_rt"] = ex.create_lazy_template( + f"the description of scope {scope_list['name']}", scope_list["description"], rich_text_context + ) # Targets for target in base_context["targets"]: if isinstance(target, dict): if target["note"]: - target["note_rt"] = ex.create_lazy_template(f"the note of target {target['ip_address']}", target["note"], rich_text_context) + target["note_rt"] = ex.create_lazy_template( + f"the note of target {target['ip_address']}", target["note"], rich_text_context + ) # Deconfliction Events for event in base_context["deconflictions"]: if isinstance(event, dict): if event["description"]: - event["description_rt"] = ex.create_lazy_template(f"the description of deconfliction event {event['title']}", event["description"], rich_text_context) + event["description_rt"] = ex.create_lazy_template( + f"the description of deconfliction event {event['title']}", + event["description"], + rich_text_context, + ) # White Cards for card in base_context["whitecards"]: if isinstance(card, dict): if card["description"]: - card["description_rt"] = ex.create_lazy_template(f"the descriptio of whitecard {card['title']}", card["description"], rich_text_context) + card["description_rt"] = ex.create_lazy_template( + f"the descriptio of whitecard {card['title']}", card["description"], rich_text_context + ) # Infrastructure for asset_type in base_context["infrastructure"]: @@ -135,9 +151,9 @@ def process_projects_richtext( if isinstance(asset, dict): if asset["note"]: asset["note_rt"] = ex.create_lazy_template( - f"the note of {asset_type} {asset.get('name') or asset['domain']}", + f"the note of {asset_type} {asset.get('name') or asset.get('domain') or asset['ip_address']}", asset["note"], - rich_text_context + rich_text_context, ) for asset in base_context["infrastructure"]["domains"]: @@ -148,25 +164,33 @@ def process_projects_richtext( # Logs for log in base_context["logs"]: for entry in log["entries"]: - ex.process_extra_fields(f"log entry {entry['description']} of log {log['name']}", entry["extra_fields"], OplogEntry, rich_text_context) + ex.process_extra_fields( + f"log entry {entry['description']} of log {log['name']}", + entry["extra_fields"], + OplogEntry, + rich_text_context, + ) @classmethod def generate_lint_data(cls): - context = {name: copy.deepcopy(LINTER_CONTEXT[name]) for name in [ - "project", - "client", - "team", - "objectives", - "targets", - "scope", - "deconflictions", - "whitecards", - "infrastructure", - "logs", - "company", - "report_date", - "extra_fields", - ]} + context = { + name: copy.deepcopy(LINTER_CONTEXT[name]) + for name in [ + "project", + "client", + "team", + "objectives", + "targets", + "scope", + "deconflictions", + "whitecards", + "infrastructure", + "logs", + "company", + "report_date", + "extra_fields", + ] + } for field in ExtraFieldSpec.objects.filter(target_model=Report._meta.label): context["extra_fields"][field.internal_name] = field.empty_value() for field in ExtraFieldSpec.objects.filter(target_model=Project._meta.label): From d4dd105502956584e2c58a3d600f1aa6593ced9d Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Tue, 23 Jul 2024 14:43:46 -0700 Subject: [PATCH 2/5] Updated admin panel to better split figure settings --- ghostwriter/commandcenter/admin.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/ghostwriter/commandcenter/admin.py b/ghostwriter/commandcenter/admin.py index f3e45a438..809c8b5ca 100644 --- a/ghostwriter/commandcenter/admin.py +++ b/ghostwriter/commandcenter/admin.py @@ -41,14 +41,21 @@ class ReportConfigurationAdmin(SingletonModelAdmin): ) }, ), + ( + "Captions", + { + "fields": ( + "title_case_captions", + "title_case_exceptions", + ) + }, + ), ( "Figures", { "fields": ( "prefix_figure", "label_figure", - "title_case_captions", - "title_case_exceptions", ) }, ), @@ -89,13 +96,11 @@ class ExtraFieldSpecForm(forms.ModelForm): user_default_value = forms.CharField( required=False, strip=True, - help_text="Value used in newly created objects. Changing this will not change existing objects, and newly created fields will be set to blank on existing objects." + help_text="Value used in newly created objects. Changing this will not change existing objects, and newly created fields will be set to blank on existing objects.", ) description = forms.CharField( - required=False, - strip=True, - help_text="Help text shown under the extra field in forms" + required=False, strip=True, help_text="Help text shown under the extra field in forms" ) def clean_user_default_value(self): From 66c5d63e45cce49888777f138911280f969a0ea6 Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Tue, 23 Jul 2024 14:44:10 -0700 Subject: [PATCH 3/5] Added `ref` tags to autocomplete options --- ghostwriter/reporting/templates/reporting/local_edit.html | 8 ++++++++ .../templates/reporting/local_observation_edit.html | 4 ++++ .../templates/reporting/report_extra_field_edit.html | 8 ++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/ghostwriter/reporting/templates/reporting/local_edit.html b/ghostwriter/reporting/templates/reporting/local_edit.html index e84cb8d8f..d286fa441 100644 --- a/ghostwriter/reporting/templates/reporting/local_edit.html +++ b/ghostwriter/reporting/templates/reporting/local_edit.html @@ -215,6 +215,10 @@ text: '\{\{.{{ file.friendly_name|escapejs }}\}\}', value: '\{\{.{{ file.friendly_name|escapejs }}\}\}' }, + { + text: '\{\{.ref {{ file.friendly_name|escapejs }}\}\}', + value: '\{\{.ref {{ file.friendly_name|escapejs }}\}\}' + }, {% endfor %} {% endif %} {% if reportfindinglink.report.evidence_set.all %} @@ -223,6 +227,10 @@ text: '\{\{.{{ file.friendly_name|escapejs }}\}\}', value: '\{\{.{{ file.friendly_name|escapejs }}\}\}' }, + { + text: '\{\{.ref {{ file.friendly_name|escapejs }}\}\}', + value: '\{\{.ref {{ file.friendly_name|escapejs }}\}\}' + }, {% endfor %} {% endif %} ]; diff --git a/ghostwriter/reporting/templates/reporting/local_observation_edit.html b/ghostwriter/reporting/templates/reporting/local_observation_edit.html index 8da5aa3b8..dc19f6371 100644 --- a/ghostwriter/reporting/templates/reporting/local_observation_edit.html +++ b/ghostwriter/reporting/templates/reporting/local_observation_edit.html @@ -136,6 +136,10 @@ text: '\{\{.{{ file.friendly_name|escapejs }}\}\}', value: '\{\{.{{ file.friendly_name|escapejs }}\}\}' }, + { + text: '\{\{.ref {{ file.friendly_name|escapejs }}\}\}', + value: '\{\{.ref {{ file.friendly_name|escapejs }}\}\}' + }, {% endfor %} ]; diff --git a/ghostwriter/reporting/templates/reporting/report_extra_field_edit.html b/ghostwriter/reporting/templates/reporting/report_extra_field_edit.html index f531d6177..8444a51ce 100644 --- a/ghostwriter/reporting/templates/reporting/report_extra_field_edit.html +++ b/ghostwriter/reporting/templates/reporting/report_extra_field_edit.html @@ -47,8 +47,12 @@

{{ report.title }}

{text: '\{\{.project_type\}\}', value: '\{\{.project_type\}\}'}, {% for evidence in report.evidence_set.all %} { - text: '\{\{.{{ evidence.friendly_name|escapejs }}\}\}', - value: '\{\{.{{ evidence.friendly_name|escapejs }}\}\}' + text: '\{\{.{{ evidence.friendly_name|escapejs }}\}\}', + value: '\{\{.{{ evidence.friendly_name|escapejs }}\}\}' + }, + { + text: '\{\{.ref {{ evidence.friendly_name|escapejs }}\}\}', + value: '\{\{.ref {{ evidence.friendly_name|escapejs }}\}\}' }, {% endfor %} ]; From 2ac130fa066a615a7ad9104fa48232e707c048c8 Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Tue, 23 Jul 2024 15:16:46 -0700 Subject: [PATCH 4/5] Enabled autocomplete and evidence on extra fields for obs & findings --- ghostwriter/reporting/forms.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ghostwriter/reporting/forms.py b/ghostwriter/reporting/forms.py index 4447bb51e..45c7ca60a 100644 --- a/ghostwriter/reporting/forms.py +++ b/ghostwriter/reporting/forms.py @@ -605,7 +605,7 @@ def __init__(self, *args, **kwargs): ) if has_extra_fields else None, - Field("extra_fields") if has_extra_fields else None, + Field("extra_fields", css_class="enable-evidence-upload") if has_extra_fields else None, ButtonHolder( Submit("submit_btn", "Submit", css_class="btn btn-primary col-md-4"), HTML( @@ -855,8 +855,12 @@ def __init__(self, user=None, *args, **kwargs): self.fields[field].widget.attrs["autocomplete"] = "off" if kwargs.get("instance"): - self.fields["client"].help_text += ". Changing this will unset this template as the global default template and the default templates on reports for other clients." - self.fields["doc_type"].help_text += ". Changing this will unset this template as the global default template and the default templates on reports." + self.fields[ + "client" + ].help_text += ". Changing this will unset this template as the global default template and the default templates on reports for other clients." + self.fields[ + "doc_type" + ].help_text += ". Changing this will unset this template as the global default template and the default templates on reports." self.fields["document"].label = "" self.fields["document"].widget.attrs["class"] = "custom-file-input" @@ -1175,7 +1179,7 @@ def __init__(self, *args, **kwargs): css_class="form-row", ), Field("description", css_class="enable-evidence-upload"), - Field("extra_fields"), + Field("extra_fields", css_class="enable-evidence-upload"), ButtonHolder( Submit("submit_btn", "Submit", css_class="btn btn-primary col-md-4"), HTML( From ea81b2c11a44f243e2a9044d1ac11ed5ba40d6e3 Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Wed, 24 Jul 2024 14:02:31 -0700 Subject: [PATCH 5/5] Updated for v4.2.3 --- CHANGELOG.md | 18 ++++++++++++++++++ VERSION | 4 ++-- config/settings/base.py | 4 ++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 934f1a70a..927c848a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ 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). +## [4.2.3] - 24 July 2024 + +### Added + +* Added support for internal hyperlinks in the WYSIWYG editor (Closes #465; thanks to @domwhewell-sage) + * You can now create internal links to headings when you insert a hyperlink, enter `#` to start your hyperlink URL, and select a heading + * Internal links will be converted to cross-references in the report template + +### Changed + +* Applied `ListParagraph` to the lists in Word reports to ensure proper paragraph styling (PR #482; thanks to @smcgu) +* The autocomplete list for keywords in reports now includes entries for `{{.ref }}` for evidence references alongside the evidence file (e.g., `{{.}}`) (Closes #479) +* Custom fields for observations and findings now support autocomplete and have the "Upload Evidence" button (Closes #485) + +### Fixed + +* Fixed an issue that could prevent reports from being generated if a related cloud server was missing a hostname (PR #481) + ## [v4.2.2] - 3 July 2024 ### Added diff --git a/VERSION b/VERSION index dfe75b0b6..bd55b1b2e 100644 --- a/VERSION +++ b/VERSION @@ -1,2 +1,2 @@ -v4.2.2 -3 July 2024 +v4.2.3 +24 July 2024 diff --git a/config/settings/base.py b/config/settings/base.py index 9f54339d8..7b1a47249 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -11,9 +11,9 @@ # 3rd Party Libraries import environ -__version__ = "4.2.2" +__version__ = "4.2.3" VERSION = __version__ -RELEASE_DATE = "3 July 2024" +RELEASE_DATE = "24 July 2024" ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent APPS_DIR = ROOT_DIR / "ghostwriter"