# Form Layouts & Design Source: https://www.ghostwriter.wiki/coding-style-guide/form-layouts-and-design Creation and layout of forms ## Introduction The Ghostwriter project uses the *django-crispy-forms* library (Crispy) to layout forms. At a basic level, this library provides template tags for rendering form fields that are more visually appealing than the regular Django form fields. [GitHub - django-crispy-forms/django-crispy-forms](https://github.com/django-crispy-forms/django-crispy-forms) Ghostwriter leverages the library's more advanced features to create `FormHelper()` and `Layout()` objects to design reusable forms in code. This requires some additional work upfront, but the end result should be a form that can be modified in one location (the *forms.py* file) and reused in multiple views and templates. If created correctly, the form can be added to a template with two lines: ```json theme={"system"} {% load crispy_forms_tags %} {% crispy form form.helper %} ``` Read about Crispy's `FormHelper()` and `Layout()` objects: ## Creating a Form Most forms should be instances of Django's `models.ModelForm` class. This makes it simpler to create a form for most, if not all, of Ghostwriter's use cases for a form. The name of the form should identify the related model and include an appropriate docstring. ```json theme={"system"} class ClientContactForm(forms.ModelForm): """ Create an individual :model:`rolodex.ClientContact` for a :model:`rolodex.Client`. """ class Meta: model = ClientContact exclude = ("client",) ``` Avoid naming forms with adjectives like "create" (ex: `ClientContactCreate`) because the forms should be reusable. A form used to create a new model entry should be reusable to update that same entry. The `Meta` class of every instance of `ModelForm` must declare the model as the variable`model` and then provide values for either `fields` or `exclude`. Use `exclude` to declare which fields should not be included in the form. If the form applies to a specific scenario where `exclude` would be a bigger list than `fields`, then use `fields` to declare which fields to include. Finally, if the form will include all fields, explicitly state this by setting `fields = "__all__"`. ### Forms.py Forms should be added to the application's *forms.py* file and then imported using a line like `from .forms import FORM_NAME`. When form files become large (like when dealing with multiple parent forms and child formsets), the files become difficult to navigate., so there is one exception to this rule: If the *forms.py* file grows into an excessively large file, split the file into two or more files organized by model. The Rolodex application's form files are a good example of this situation. All of the forms related to the `Client` and `Project` models and their associated child models pushed the single *forms.py* file beyond 1,000 lines. To make it easier to maintain each set of forms, the project moved them into files named \_forms.*client.py* and *forms\_project.py*. Name the new files with the *forms*\_ prefix followed by the model's name. ## Basic Form Design The Django form attributes control field attributes. The Crispy `FormHelper()` and `Layout()` objects control the `
` tags and the layout of the fields, respectively. ### Setting Field Attributes Django allows for field attributed to be configured in the form class. For `ModelForm` instances, this is done in the form's `__init__` method. Attributes should be set at the top of the `__init__` method, like so: ```json theme={"system"} def __init__(self, *args, **kwargs): super(ClientContactForm, self).__init__(*args, **kwargs) self.fields["name"].widget.attrs["placeholder"] = "David McQuire" self.fields["name"].widget.attrs["autocomplete"] = "off" self.fields["email"].widget.attrs["placeholder"] = "info@specterops.io" self.fields["email"].widget.attrs["autocomplete"] = "off" self.fields["job_title"].widget.attrs["placeholder"] = "CEO" self.fields["job_title"].widget.attrs["autocomplete"] = "off" self.fields["phone"].widget.attrs["placeholder"] = "(800) 444-4444" self.fields["phone"].widget.attrs["autocomplete"] = "off" self.fields["note"].widget.attrs[ "placeholder" ] = "Additional notes for the contact" ``` A `placeholder` attribute should be set for all fields. The placeholder text should provide an example of the intended content or an example of the proper input format. In more freeform fields, the placeholder should identify the field and guide the user towards its intended purpose (e.g., the `note` field in the above example). All fields should set `autocomplete` to `off` unless it is explicitly needed. Otherwise, autocomplete behavior can negatively impact user experience (e.g., autocomplete lists appearing and covering datepickers) or display non-public information when clicking on the field. The latter is mostly an issue for demonstrations of Ghostwriter. ### FormHelper Object Every form should have a `FormHelper()` object named `self.helper`. Create a `FormHelper()` object named in the form's `__init__` method. At a minimum, Ghostwriter form helpers set several values. These values ensure the form appears correctly in the content of the rendered webpage. ```json theme={"system"} # Design form layout with Crispy FormHelper self.helper = FormHelper() # Explicitly turn on/off
tags for the form self.helper.form_tag = True # Explicitly state if labels should be displayed self.helper.form_show_labels = False # Set a class for the form from the stylesheet self.helper.form_class = "newitem" ``` The form labels should be hidden if the form is easy to understand from placeholders or context. Formsets require some additional modifications to the `FormHelper()` configuration. See the next section for more information. ### Layout Object Every form should have a `Layout()` object assigned to the `FormHelper()` object's `layout` attribute, `self.helper.layout`. This object controls the form's HTML. Crispy uses a collection of HTML templates assigned to different `crispy_forms.layout` and `crispy_forms.bootstrap` classes to generate elements when the form is rendered. Many of these classes take all kwargs and pass them to the HTML templates as attributes. This means adding something like `id="my-div-id"` to an instance of `Div()` will result in Crispy using this to the render the div and set the div's `id` attribute to *my-div-id*. Some HTML attributes are keywords in Python, like `class`, so they require using a different argument. To set the class attribute, use `css_class`. This is powerful and makes it easy to maintain and modify the form, but it does mean style changes require more than saving a template and refreshing the webpage. While in `DEBUG` mode for development, every save action will restart the server. Plan form changes carefully to avoid excessive wait times for restarting the server. The layout for a basic `ClientNote` form might look like this example. This form display a single `TextArea` for a note. The only other fields are hidden fields used for associating the entry with an individual `Client` and individual `Users`. ```json theme={"system"} def __init__(self, *args, **kwargs): super(ClientNoteForm, self).__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_method = "post" self.helper.form_class = "newitem" self.helper.form_show_labels = False self.helper.layout = Layout( Div("note", "operator", "client"), ButtonHolder( Submit("submit-button", "Submit", css_class="btn btn-primary col-md-4"), HTML( """ """ ), ), ) ``` All of the fields are inside of an instance of the `Div()` class so they appear wrapped in `
` tags. While not used in this example, this allows for specifying a CSS class and other attributes for the div. The form concludes with a button to save the note and a button to cancel and leave the form. #### Button Controls Every form must end with at least two buttons, a submit button to save the entry and a cancel button to abandon the form. Assign the Submit button these CSS classes: `css_class="btn btn-primary col-md-4"` Assign the Cancel button these CSS classes: `css_class="btn btn-outline-secondary col-md-4` The submit button should be an instance of Crispy's `Submit()` class. This class accepts a name and a value. In most examples online the `name` parameter is set to *submit*. The name can be anything, but using *submit-button* is generally preferred. The value should always be *Submit*. In many examples, including Crispy's own documentation, the `value` parameter is set to `submit`. This works in most cases, but can cause unintended issues if the form is ever used with JavaScript. Setting the name of any field or button to *submit* masks the form's `submit()` method. Do not name submit buttons *submit*. Ghostwriter passes a `cancel_link` context variable to templates with forms. This variable contains a pre-defined URL that will return the user to a sensible location if they choose to abandon the form. Use Crispy's `HTML()` class to create a button with an `onclick` attribute that uses the `cancel_link` context variable and sets the other necessary values for the button. ```json theme={"system"} HTML( """ """ ), ``` The cancel button could be an instance of `Button()` with an `onclick` kwarg, but Django will not render context variables passed to the template in this way. #### Organizing Large Forms Large forms can be unwieldy, especially if the form contains one or more inline formsets that users can add to the form to make it longer. Large forms should be organized into Bootstrap tabs using Crispy's `Tabholder()` class. This class must contain tabs that hold different sections of the form. Ghostwriter does not use Crispy's `Tab()` class. Instead, there is a `CustomTab()` class available in the project that allows for additional customization of the tabs. # Custom Layout Objects Source: https://www.ghostwriter.wiki/coding-style-guide/form-layouts-and-design/custom-layout-objects The Ghostwriter project's custom additions to django-crispy-forms # Formset Layout & Design Source: https://www.ghostwriter.wiki/coding-style-guide/form-layouts-and-design/formset-layout-and-design Creation and layout of inline formsets # Overview Source: https://www.ghostwriter.wiki/coding-style-guide/style-guide Coding style guide for the Ghostwriter code base ## Introduction To maintain consistency as the project develops, all contributors to the project should keep the following requirements in mind before committing code or submitting a pull request. Using Visual Studio Code (VSCode) makes it easier to follow this style guide. If a developer uses a different editor, pre-commit hooks are provided at the bottom of the page. ## Code Formatting The Ghostwriter project uses Python Black to enforce code style. Logo Microsoft's official Python extension for VSCode supports formatters like Black. Microsoft's documentation covers this topic: [https://code.visualstudio.com/docs/python/editingcode.visualstudio.com](https://code.visualstudio.com/docs/python/editing) Black is "opinionated" and automatically changes things to keep code consistent – like intelligently changing single quotes to double quotes. 1. Install the Python extension in VSCode (SHIFT+CMD+X and search for `python`) 2. Install Black for the Python interpreter or virtual environment selected for VSCode 3. Add the following settings to VSCode's *settings.json*: ```json theme={"system"} "editor.formatOnSave": true, "python.formatting.provider": "black", "python.formatting.blackArgs": [ "--line-length", "90" ], ``` VSCode will now use Black to format all Python files on save actions. ### Managing Whitespace Black will not automatically delete trailing whitespace or whitespace on otherwise empty lines. Trailing whitespace will be identified by the linter (below). Add this line to VSCode's *settings.json* file to automatically delete trailing whitespace on save: ```json theme={"system"} "files.trimTrailingWhitespace": true ``` ## Managing Imports The Ghostwriter project also uses `Isort` which is part of the Python extensions `Python Refactor` toolkit (`Python Refactor: Sort Imports`). [![Logo](https://pypi.org/static/images/favicon.6a76275d.ico)isortPyPI](https://pypi.org/project/isort/) This tool sorts imports by library types (FUTURE, STDLIB, THIRDPARTY, FIRSTPARTY, LOCALFOLDER) and then alphabetically. It is customizable to generate comments and sort by line length. Ghostwriter code repository includes an *.isort.cfg* file. Ghostwriter's current configuration contains: ```json theme={"system"} [settings] profile=ghostwriter src_paths=isort,test atomic=True line_length=90 use_parentheses=True ensure_newline_before_comments=True import_heading_stdlib=Standard Libraries import_heading_firstparty=Ghostwriter Libraries import_heading_thirdparty=Django & Other 3rd Party Libraries ``` This configuration enforces the use of parentheses, adds newlines between sections, and keeps the line length /\<= 90 characters. It also adds custom comments before standard libraries, third-party libraries, and Ghostwriter's local first-party libraries. Here is an example: ```json theme={"system"} # Ghostwriter Libraries from ghostwriter.modules import codenames from .filters import ClientFilter, ProjectFilter from .forms import ( AssignmentCreateForm, ClientContactCreateForm, ClientCreateForm, ClientNoteCreateForm, ProjectAssignmentFormSet, ProjectCreateForm, ProjectForm, ProjectNoteCreateForm, ProjectObjectiveCreateForm, ProjectObjectiveFormSet ) from .models import ( Client, ClientContact, ClientNote, ObjectiveStatus, Project, ProjectAssignment, ProjectNote, ProjectObjective ) ``` Once the Python extension is installed, run `isort` by pressing SHIFT+CMD+P and selecting `Python Refactor: Sort Imports`. ## Line Length The Ghostwriter project enforces a 90 to 119-character line length limit. The PEP-8 style guide says to limit lines to 79-characters, but that leads to longer files that use half the horizontal space. The Django Project enforces 119-character lines because that's the maximum characters displayed (without scrolling) by GitHub's code viewer. Black defaults to 88, but says "90-ish is a wise choice." See here: [https://black.readthedocs.io/en/stable/the\_black\_code\_style.html#line-lengthblack.readthedocs.io](https://black.readthedocs.io/en/stable/the_black_code_style.html#line-length) > You probably noticed the peculiar default line length. *Black* defaults to 88 characters per line, which happens to be 10% over 80. This number was found to produce significantly shorter files than sticking with 80 (the most popular), or even 79 (used by the standard library). In general, [90-ish seems like the wise choice](https://youtu.be/wf-BqAjZb8M?t=260). The Ghostwriter project does not use 88 because it is registered as a numerical hate symbol by the Anti-Defamation League. `Isort` defaults to 79 to match PEP-8, so a line length must be configured to avoid style conflicts. For these reasons, the Ghostwriter project requires maximum line length be between 90 and 119-characters to keep everything comfortable to read in code editors and on GitHub. Any lines shorter than 90 should not be split, and longer lines should not exceed 119-characters without reason. ## Docstrings The Ghostwriter project requires consistent docstrings for all views, functions, forms, models, and other classes and objects. Ghostwriter's docstrings deviate from PEP-8 in favor of Django's style. Django can read docstrings and generate documentation. That only works for database models and views, but the style should be applied to other parts of the project for consistency. See Django's documentation: [![Logo](https://static.djangoproject.com/img/icon-touch.e4872c4da341.png)The Django admin documentation generator | Django documentation | Django](https://docs.djangoproject.com/en/3.0/ref/contrib/admin/admindocs/) A good view docstring looks like this: ```json theme={"system"} class ClientDetailView(LoginRequiredMixin, generic.DetailView): """ Display an individual :model:`rolodex.Client`. **Context** ``domains`` List of :model:`shepherd.Domain` associated with :model:`rolodex.Client`. ``servers`` List of :model:`shepherd.StaticServer` associated with :model:`rolodex.Client`. ``vps`` List of :model:`shepherd.TransientServer` associated with :model:`rolodex.Client`. **Template** :template:`rolodex/client_detail.html` """ ``` Note the newline after the opening `"""` which deviates from standard practice (per PEP-8). Further, the use of grave accents, asterisks ( \* ), and colons ( : ). are all purposeful and important. Django and `docutils` convert these symbols into formatting for the auto-generated documentation in the admin panel. The above example is rendered like this: ![](https://www.ghostwriter.wiki/~gitbook/image?url=https%3A%2F%2F1408755273-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-legacy-files%2Fo%2Fassets%252F-LlOJdrSdgKfLf_tEm3S%252F-MCTv60mzzLBgvfNOSj6%252F-MCU1XqjHK6x0DR10h2W%252Fimage.png%3Falt%3Dmedia%26token%3D528a4a2d-fef2-43dd-b61a-b986bfa2649c\&width=768\&dpr=4\&quality=100\&sign=71a427b\&sv=2) Anything wrapped in grave accent marks will be transformed into a link leading to the related object (e.g., a model). When seeking to emphasize a variable, function name, or something else that should not become a link, wrap the string in double grave accent marks, as seen around the context variables in the above example. Likewise, asterisks will cause a string to be rendered as a header. Do not wrap anything in asterisks that should not become a header for a section. Follow the documentation for the Django admin documentation generator to see how headers are used. Finally, docstrings should have newlines between sections and section headers. These newlines should be completely empty – i.e., they should have no whitespace. Setup VSCode snippets to create templates for repeated code snippets, like docstrings. ```json theme={"system"} "UpdateView Docstring": { "prefix": "update", "body": [ "\"\"\"", "Update an individual :model:`${1:model}`.", "", "**Template**", "", ":template:`${2:template}`", "\"\"\"", ], "description": "A docstring template for an UpdateView" }, ``` ## Linting The Ghostwriter project recommends using `flake8` to lint Ghostwriter's code. The VSCode Python extension natively supports linting and a variety of linters. [https://code.visualstudio.com/docs/python/lintingcode.visualstudio.com](https://code.visualstudio.com/docs/python/linting) The project recommends changing VSCode's default `PyLint` to `flake8` because this linter is much faster and snappier – especially with some of Ghostwriter's longer Python files (e.g., a *views.py*). The `flake8` linter is logical and stylistic, like `PyLint`. Black should handle most of the linting, but it won't flag unused imports. When the linter returns errors or warnings, VSCode changes the filename to yellow or red. The editor also displays squiggles under the affected lines. Address all linting issues before committing any code. At a minimum, eliminate trailing whitespace and remove unused imports. ## Pre-commit Hooks In lieu of the VS Code extensions and configurations, developers can use pre-commit hooks to catch style guide violations. Find the project documentation here: Logo From the documentation's introduction: > Git hook scripts are useful for identifying simple issues before submission to code review. We run our hooks on every commit to automatically point out issues in code such as missing semicolons, trailing whitespace, and debug statements. By pointing these issues out before code review, this allows a code reviewer to focus on the architecture of a change while not wasting time with trivial style nitpicks. First, install the library by running `pip install pre-commit` for your local development environment. Add a `.pre-commit-config.yaml` file to the project's root directory. Use the example below as a model for this file. Once the file is in place, run `pre-commit install` to hook future git commits. ```json theme={"system"} exclude: 'docs|node_modules|migrations|.git|.tox' default_stages: [commit] fail_fast: true repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: master hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - repo: https://github.com/psf/black rev: 19.10b0 hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.3 hooks: - id: flake8 args: ['--config=setup.cfg'] additional_dependencies: [flake8-isort] ``` # Overview Source: https://www.ghostwriter.wiki/configuring-global-settings/configuring-apis Enabling and configuring API access for tasks and notifications Ghostwriter supports a growing number of APIs that the application leverages to perform background tasks, monitor infrastructure, and send helpful notifications. All APIs are optional, but you should consider configuring them to get the most out of Ghostwriter's features. The supported APIs are: # Configuring Cloud Services Source: https://www.ghostwriter.wiki/configuring-global-settings/configuring-apis/configuring-cloud-services Enabling cloud service APIs Ghostwriter can track cloud resources used for projects. If you provide access tokens for Amazon Web Services (AWS) and Digital Ocean (DO), Ghostwriter has a task that will collect all running server instances and check if any of them are attached to a completed project. The task will report back with JSON detailing your active (powered-on) cloud servers. If you have Slack enabled, Ghostwriter will send notifications to let you know if an active cloud server is not tracked as part of a project or is tracked as part of a project that has ended. ### Ignoring Specific Assets You may spin up cloud servers on the same account that you do not want to be monitored. You can tag these servers with an "ignore tag" of your choosing. Provide a comma-separated list of tags for Ghostwriter to ignore when checking cloud assets. If you use Slack notifications to send reminders to teardown cloud infrastructure, you can also provide a notification delay in days. Ghostwriter will wait to send reminders until a project's end date + your delay. This task is under development to support monitoring Microsoft Azure and additional AWS resources (e.g., Elastic IPs). ### Configuring AWS Keys Ghostwriter is designed to use minimal AWS privileges so you do not need to assign any serious privileges to the keys you use for monitoring your AWS resources. Ghostwriteraccesses and reads from the following services: * STS – Ghostwriter connects and calls `get-caller-identity` to test your keys * Lightsail – Collects your running Lightsail instances and related identifiers * EC2 – Collects your running EC2 instances and related identifiers * S3 – Collects your list of buckets Keep Ghostwriter's privileges limited. Ghostwriter does not need to be able to upload files to S3 or modify instances, or access storage volumes. The monitoring task only needs to read resource information (i.e., use "Get," "List," and "Describe" permissions). Fetching instance information from Lightdail and EC2 requires specifying a region. To determine which regions your account has enabled, Ghostwriter calls EC2's `describe-regions` and Lightsail's `get-regions`. Then, Ghostwriter uses an EC2 resource to call `instances` and a Lightsail client to call `get-instances` to build a list of instances. This data includes: * ARN * Name * State * Private IP(s) * Public IP(s) * LaunchTime * Tags * Misc. Networking and Hardware For S3, Ghostwriter calls `list-buckets` to get a list of all buckets. This data includes the bucket's name and the date it was created. # Configuring Namecheap Source: https://www.ghostwriter.wiki/configuring-global-settings/configuring-apis/configuring-namecheap Enabling the Namecheap API If you use Namecheap to register domain names, Ghostwriter can use Namecheap's API to sync your domain registrations with the Domain Library in Ghostwriter. You can setup this synchronization task to run on a schedule or execute it manually. On the first run, the task will populate your Domain Library. Subsequent runs will update records and automatically add new domain purchases and expire domain names that have dropped-off your Namecheap account. Fill-in the configuration values in accordance with your API configuration. All of the relevant details are in your Namecheap dashboard. You can learn more here: Logo Namecheap.com # Configuring Slack Source: https://www.ghostwriter.wiki/configuring-global-settings/configuring-apis/configuring-slack Enabling Slack WebHooks for notifications If enabled, Ghostwriter will use Slack to send notifications and reminders to configured channels. The configuration requires an incoming Webhook. Slack API Specify a username and emoji for the bot. Emojis must be set using Slack syntax like (e.g., `:ghost:`). You can use any emoji available in your Slack team – including custom emojis. The username can be anything you could use for a Slack username. The emoji will appear as the bot's avatar alongside the username. The alert target is the message target. You can set this to a blank string or a user, aliases, or @here/@channel. Slack username/alias targets must be written as ``, ``, or `<@username>` for them to work as actual notification keywords. Finally, set the target channel. This might be your `#general` or some other channel. This is the global value Ghostwriter will use for all messages unless a project-specific channel is supplied. When users create a new project, there is an option to provide a Slack channel for project-related notifications. # Configuring VirusTotal Source: https://www.ghostwriter.wiki/configuring-global-settings/configuring-apis/configuring-virustotal Enabling the VirusTotal API Ghostwriter can use VirusTotal's API to look up domain names in your Domain Library. If any domain name is linked to positive malware downloads or assigned an undesirable category, Ghostwriter will mark that domain as "Burned" with an explanation. The blocklist is maintained inside of the *review\.py* module: ```python Domain Blocklist theme={"system"} class DomainReview(object « snip » blocklist = [ "phishing", "web ads/analytics", "suspicious", "placeholders", "pornography", "spam", "gambling", "scam/questionable/illegal", "malicious sources/malnets", ] ``` By default, Ghostwriter will block users from checking out and using domain names marked as burned. You can always review the status change and override it (change the status back to "Healthy") if you feel the domain is still safe to use. If you do not have one, get [a free API key from VirusTotal](https://developers.virustotal.com/reference). The free version (VirusTotal Community / Public API) is limited to 500 requests per day and 4 requests per minute. By default, Ghostwriter sleeps for 20 seconds between requests for 3 requests per minute. If your organization has a premium API key (aka Premium API or Private API), you can change the sleep time to match the key's configured request rate. Some older API keys may have more restrictive quotas. You can check your key's quotas by visiting *[https://virustotal.com/](https://virustotal.com/)*, logging in with the account linked to the key, clicking your profile, and clicking "API Key" from the menu. Under the VirusTotal Configuration section, check the *Enable* checkbox and provide your API key. Only change the *Sleep Time* value if your rate limit allows for faster requests. # Configuring Extra Fields Source: https://www.ghostwriter.wiki/configuring-global-settings/configuring-extra-fields Ghostwriter v4.1 introduces Extra Fields, additional user-defined fields that can be added to several objects that Ghostwriter stores for data specific to your own workflow. ### Configuring To configure extra fields, go to Admin Panel > Admin Panel, and then "Extra Field Configurations". Then select the object type you wish to configure extra fields for. Each extra field has the following options: * Internal Name: Name used to store the field internally, as well as in templates. Cannot contain spaces, and must be unique across all fields for an object type. * Display Name: Name that is used in the UI for the field. Spaces are allowed here. * Type: The data type of the field. Currently this includes: * **Checkbox**: Single true/false checkbox * **Single Line of Text**: Single line of unformatted text * **Formatted Text**: Multiple lines of text with formatting * **Integer**: Whole integer value without any decimal component * **Number**: Numeric value, potentially with a decimal component After saving the field, it will appear in the object's edit forms and in their detail pages: ### Using in Templates In templates, an object's extra fields are stored as subattributes of the `extra_fields` attribute. For example, to display the field with the internal name `internal_contact` on a Client stored on the variable `client`, you can write `{{client.extra_fields.internal_contact}}`. The extra field may be `None` internally if the extra field was created after the object was, and has not been set yet. # Configuring Global Report Options Source: https://www.ghostwriter.wiki/configuring-global-settings/configuring-global-report-options Configuring report styles Ghostwriter's report engine pulls several default style settings from Command Center. The settings apply to all projects and reports. ## Report Configuration The Global Report Configuration section contains options for customizing the contents of your Word documents. Due to limitations in the PowerPoint API (and the nature of PowerPoint compared to Word), options for borders, tables, and figures only apply to Word documents right now. ### Borders If you enable borders for pictures, you can set a border line weight (in EMUs; default is `12700` EMUs, or 1pt) and a border color code (e.g., `2D2B6B`). ### Tables and Figures Ghostwriter will add cross-references (e.g., bookmarks) for figures and tables. Configure a label and a separator character that will appear between your label and your captions. The default separator is an en dash (–). You can also enable automatic title casing of captions. There is an exception list for words you do not want to be capitalized, such as articles. ### Report Generation Options Finally, you can select default report templates, configure a target delivery date (in business days), and configure a default filename for new report downloads. The filenames can be generic or include placeholders to create dynamic filenames. Filenames can use the following placeholder strings or [date formatting characters](https://docs.djangoproject.com/en/4.1/ref/templates/builtins/#date): | **Placeholder** | **Description** | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `{date}` | The current date formatted with your [configured date format](https://www.ghostwriter.wiki/getting-started/quickstart#customizing-the-date-format). | | `{client}` | The name of the client associated with the project. | | `{company}` | Your configured company name ([Company Information](/configuring-global-settings/personalizing-company-information)). | | `{assessment_type}` | The assessment type set for the project. | The default filename value is a good example of using these dynamic elements. The default value is: `{Y-m-d`}`_{His} {company} - {client} {assessment_type} Report` That translates into a string like this: `2022-11-02_185117 SpecterOps - Ghostwriter Red Team Report` ### Severity Categories and Color Severity categories are managed under the *Reporting* application and the *Severity* model. Set a color code for each severity category (e.g., `966FD6`). Do not change the name or weights without updating the HTML templates. Some templates modify styles based on specific severity names. Categories cannot be sorted alphabetically, so weights maintain the ordering in reports. # Configuring the Announcement Banner Source: https://www.ghostwriter.wiki/configuring-global-settings/configuring-the-announcement-banner Configuring an announcement banner for Ghostwriter. Ghostwriter supports an announcement banner that admins can use to display important messages to users. This feature is useful for notifying users about updates, maintenance, or other significant information. The banner offers a configurable title, message, and link. You can also choose to display the banner only to authenticated users or all visitors (i.e., display it even on the login page). You can also set an expiration date for the banner. It will automatically disappear after the configured date and time. If a user clicks the "x" button to close the banner, it will not reappear for that user until the next time the banner changes. This allows users to dismiss the banner so it doesn't appear every time they visit the site or a new page. # Introduction to Command Center Source: https://www.ghostwriter.wiki/configuring-global-settings/introduction-to-command-center Configuring global variables for Ghostwriter and report generation ## Introducing Global Configurations An administrator can update global configuration at any time in the Django admin panel under the *Command Center* section. Visit */admin/commandcenter/* to view the current configuration on your server. Visit each configuration and personalize your settings. The Ghostwriter server does **not** need to be restarted for changes to take effect. ### Reporting Options * Company Information Configuration * Global Report Configuration ### Customizing Models * Extra Fields Configuration ### Monitoring * VirusTotal Configuration * Cloud Services Configuration ### Syncing & Updating * Namecheap Configuration ### Notifications * Slack Configuration If you don't use Slack, you can customize notifications and tasks in *tasks.py*. You may also consider using the GraphQL API to create your own integrations. See [Background Tasks](/features/background-tasks) and [GraphQL API](/features/graphql-api) for more information. # Personalizing Company Information Source: https://www.ghostwriter.wiki/configuring-global-settings/personalizing-company-information Configuring your company's information for reporting The Company Information settings affect reports. Ghostwriter can dynamically insert your company's name, social media handle, or email address. Update these values in Command Center. # Contributing to the Project Source: https://www.ghostwriter.wiki/development/contributing-to-the-project How to go about contributing code to the project ## Becoming a Contributor The Ghostwriter team welcomes contributions on GitHub.The team is open to discussing ideas for changes or enhancements and will review code contributions. ### Contributing Ideas If you want to discuss an idea, submit a GitHub Issue. Before submitting an issue, please check the following: 1. Has the idea (or something similar) been discussed on GitHub already? 2. Is the idea (or something similar) tracked on the [Trello board](https://trello.com/b/sF4om6Fy/ghostwriter)? If yes, please consider joining that discussion. If the idea has not been discussed, please clearly describe/answer the following: * What is the problem you are trying to solve? * What do want to accomplish with the change? * Do you have an example of how your idea might be implemented? The team can't address every idea or issue immediately but does accept pull requests. If you feel up to it, consider implementing your idea and submitting the code for review. ### Contributing Code The Ghostwriter project gladly accepts pull requests (PR). All PRs must include the following: * Your code, based on the latest `master` branch * Clear statement describing what the changes and why * List of documentation changes (if any) that will need to be made if the PR is merged * New or updated unit tests covering your additions or changes Your PR will be automatically reviewed by GitHub Actions. The PR must pass these CI/CD checks before it can be accepted by a member of the project team. Make sure any code contributions that introduce new features or functionality to the core project are vendor agnostic and flexible. A good PR introduces something that many people are likely to find useful or beneficial. A not-so-good PR is something specifically tailored to a certain workflow or assumes a certain vendor (e.g., registrar) or service (e.g., API, chat app). PRs like that would be better suited as a plugin. # Pre-Release Checklist Source: https://www.ghostwriter.wiki/development/contributing-to-the-project/pre-release-checklist Step-by-step instructions for preparing a release or PR ## Preparing New Code for a Release So, you want to release some code? The Ghostwriter release workflow is simple to follow. The developer of the new code must perform a few actions: * Update or create unit tests that cover their changes to the codebase * Update documentation (this wiki), as needed * Write an itemized CHANGELOG of their changes Everything else is handled automatically by the continuous integration pipeline. The following sections walk you through what to do to prepare for a new release or a pull request. ### Prepare for a Release or Pull Request 1. Review any linter alerts in VS Code (under the *PROBLEMS* tab in the console) 1. See the [Code Style Guide](/coding-style-guide/style-guide) for information about linting and proper formatting 2. Review and update wiki documentation as needed 1. The Ghostwriter team should update the wiki immediately via a commit or edit in GitBook 2. External contributions should submit changes as a pull request to the [documentation repository](https://github.com/GhostManager/Documentation) 3. Write or update unit tests for changes 4. Run all unit tests with Python *Coverage*: `docker-compose -f local.yml run django coverage run manage.py test` 5. Review Coverage report for changes in "missing" 1. Run a report with "missing" displayed: `docker-compose -f local.yml run django coverage report -m` 2. Look for code branches not covered by unit tests (e.g., `except` blocks) 6. If *Coverage* reports testing gaps in new or changed code, return to step 3 ### Ready to Create a Release or Pull Request 1. Merge feature branches into a `dev` branch 2. Test deployment of the new branch on a development server 3. Test new and changed features and anything potentially affected by the changes 1. Browse the user interface and interact with anything related to the changes 2. If the UI has changed, thoroughly test different scenarios and observe JavaScript behavior 4. If all is well, merge into `main` or create the pull request with an itemized *CHANGELOG* 5. Wait some time (usually \~25 minutes) and review the results of the GitHub Actions 6. If required Actions succeeded, the code may be merged and Ghostwriter team members will review the code prior to a release # Database Models Source: https://www.ghostwriter.wiki/development/database-models UML diagrams for Ghostwriter database models ## The Ghostwriter Database Ghostwriter uses multiple Django sub-applications to organize the codebase. This map shows all of the relationships between each app and the various models within each app. # API Models Source: https://www.ghostwriter.wiki/development/database-models/api-models The API application contains everything needed for the GraphQL API. # Configuration Models Source: https://www.ghostwriter.wiki/development/database-models/configuration-models The CommandCenter application contains models based on a custom singleton model class. The models manage global configurations and only allow one entry per model. # Home & User Models Source: https://www.ghostwriter.wiki/development/database-models/home-models The **Home** \*\*application extends Django's *User* model to add support for avatar images. The **User** \*\*application augments Django's *User* model to add fields for contact information, timezone, and roles used for role-based access controls in the API. # Infrastructure Models Source: https://www.ghostwriter.wiki/development/database-models/infrastructure-models The infrastructure manager application, **Shepherd**, and related models track everything related to servers and domain names. # Oplog Models Source: https://www.ghostwriter.wiki/development/database-models/oplog-models The **Oplog** application contains everything related to operational logging for projects. ## Sanitization Audit Models `OplogSanitization` is an immutable audit record for a completed, user-requested activity-log sanitization. It belongs to an `Oplog` and stores the database timestamp, the requesting user and display-name snapshot, and the selected fields. An activity log can have many sanitization records through its `sanitizations` relationship. `OplogEntry.updated_at` is maintained by PostgreSQL for inserts and material entry updates. It is indexed with the entry's activity log foreign key so the application can efficiently compare the newest entry update with the newest sanitization without storing a denormalized timestamp on `Oplog`. Tag-only changes do not affect this timestamp because tags are not entry content sanitized by this feature. # Reporting Models Source: https://www.ghostwriter.wiki/development/database-models/reporting-models The reporting application, **Reporting**, and related models track everything related to findings, observations, and reports. # Client & Project Models Source: https://www.ghostwriter.wiki/development/database-models/rolodex-models The client and project management application, Rolodex, and related models track everything related to a client and their projects. # Expected Services & Processes Source: https://www.ghostwriter.wiki/development/expected-services-and-processes 1. [Development](/development) If you would like to monitor or check the various processes and services running inside the containers, look for these processes. ## Processes Manually list processes with `docker-compose` and these commands: ```log theme={"system"} docker-compose -f local.yml top django docker-compose -f local.yml top postgres docker-compose -f local.yml top queue docker-compose -f local.yml top redis ``` ### Django ``` /bin/sh /start /usr/local/bin/python /usr/local/bin/uvicorn config.asgi:application --host 0.0.0.0 --reload /usr/local/bin/python -B -c from multiprocessing.resource_tracker import main;main(4) /usr/local/bin/python -B -c from multiprocessing.spawn import spawn_main; spawn_main(tracker_fd=5, pipe_handle=7) --multiprocessing-fork ``` ### Postgres ``` postgres postgres: checkpointer postgres: background writer postgres: walwriter postgres: autovacuum launcher postgres: stats collector postgres: logical replication launcher postgres: postgres ghostwriter 172.20.0.2(55562) idle postgres: postgres ghostwriter 172.20.0.2(50952) idle postgres: postgres ghostwriter 172.20.0.6(45862) idle postgres: postgres ghostwriter 172.20.0.2(41776) idle postgres: postgres ghostwriter 172.20.0.2(41794) idle postgres: postgres ghostwriter 172.20.0.5(38720) idle ``` ### Queue ``` /bin/sh /start-queue python manage.py qcluster python manage.py qcluster python manage.py qcluster python manage.py qcluster python manage.py qcluster python manage.py qcluster python manage.py qcluster python manage.py qcluster python manage.py qcluster python manage.py qcluster python manage.py qcluster python manage.py qcluster ``` ### Redis ``` redis-server ``` # Overview Source: https://www.ghostwriter.wiki/development/modifying-code Modifying and then deploying code changes ## Local Development While running Ghostwriter using `local.yml`, you can make changes on the fly without needing to run any Docker commands. Django smartly restarts the server when it detects a file change. Also, the local deployment mounts Ghostwriter's directory on your host, so changes to the code (Python or templates) will be live once the file is saved. The `build, stop, rm, up -d` commands need to be run only if you add a new library or if you add a new task to **tasks.py** **(only after initially adding the new function, not each time you change the function).** A new queued task must also be added to `GHOSTWRITER_DJANGO_Q_INTERNAL_TASKS` or `GHOSTWRITER_DJANGO_Q_SCHEDULE_TASKS`, as appropriate. `docker-compose -f local.yml stop; docker-compose -f local.yml rm -f; docker-compose -f local.yml build; docker-compose -f local.yml up -d` In some cases, a code change that causes an error at startup may persist even after you fix the problem. If the container appears "stuck" and isn't responding, issue the`up -d` command. ### Modifying Models If a model is added, updated, or deleted, the model changes must be migrated before they will go live in the application. This is accomplished using the Docker `run` command to execute the usual Django migration commands with **manage.py**: `docker-compose -f local.yml run --rm django python manage.py makemigrations` `docker-compose -f local.yml run --rm django python manage.py migrate` No additional work is required. Once a migration is successful, your model changes will be live. ### Troubleshooting Most errors will be displayed in your web browser thanks to the verbose error output configured for local deployments. Of course, there will be no visible output in your browser if an error prevents Django from starting the server. To view logging output from Redis, PostgreSQL, and Django, use the Docker `logs` command: ```log theme={"system"} docker-compose -f local.yml logs docker-compose -f local.yml logs django docker-compose -f local.yml logs queue docker-compose -f local.yml logs redis docker-compose -f local.yml logs postgres ``` Logs are handy for performing dry runs of scheduled tasks. Anything you \`print\` will be output to the logs. Add the name of the container after \`logs\` to get logs just from that service. ### Cleaning Up Containers If you break something or want to start from scratch, stop all of your containers and run these two commands: The following assumes only Ghostwriter container and volumes are present on your system. Proceed with caution if you use Docker for other things on your development system. ```log theme={"system"} docker system prune -a docker volume rm -f $(docker volume ls) ``` The first command will remove all stopped containers. If you only need to re-build, you can stop there and build your containers again. The second command deletes all volumes. The volumes hold your data (e.g., the database and files). If you leave them intact, your new container will launch ready to use with your existing data. You'll need to delete the volumes to get a true fresh start. # Modifying Environment Variables Source: https://www.ghostwriter.wiki/development/modifying-code/modifying-environment-variables Making changes to the environment variables inside the containers ## Introduction to the Environment Variables Ghostwriter CLI manages a DotEnv (`.env`) file inside the *Ghostwriter* directory. Docker sees this file and reads it when building and bringing containers up. Docker will import and set the environment variables inside the containers. The variables must first be declared in the Docker Compose files (e.g., `production.yml`). Some of the variables influence the Docker builds. The majority of the variables affect configurations of the services like Django and Hasura. Most variables have a service name as their prefix to make it easy to determine which variable affects which services. For example, the `DJANGO_SUPERUSER_PASSWORD` variable has the `DJANGO_` prefix to signify it is tied to the Django service and container. It is declared in the YAML files like this: ```yaml theme={"system"} - DJANGO_SUPERUSER_PASSWORD=${DJANGO_SUPERUSER_PASSWORD} ``` When you run the `install` command, Ghostwriter CLI executes `manage.py createsuperuser --no-input` to tell Django to create a new superuser. The `--no-input` flag tells Django to look at environment variables for username and password input. ### Variables Aliases Some of the variables have aliases. These are special variables that users are more likely to interact with via Ghostwriter CLI. Generally, the aliases are the same as the full variable name but drop the service prefix. The Ghostwriter CLI code sets these aliases so new aliases cannot be created without modifying the code and rebuilding the CLI. For example, `DATE_FORMAT` is an alias for `DJANGO_DATE_FORMAT` to make it easy for a user to change their desired date format. Docker only uses `DJANGO_DATE_FORMAT`. When a user changes `DATE_FORMAT`, Ghostwriter CLI also updates `DJANGO_DATE_FORMAT`. ## Adding New Variables There are two things you must do when adding a new variable to any container: 1. Add the variable to the DotEnv file 2. Edit the YAML files to add your environment variable under the `environment` key for the container that needs the variable You may need to do one additional thing if adding a new variable to the Django container. Open the *Ghostwriter/config/settings* directory and edit *base.py* (if adding a variable for dev and production), *local.py* for dev environments, or *production.py* for production environments. Add your new variable in the same manner as the other variables already in these files. Looking at `DJANGO_DATE_FORMAT` again, add the variable like this with `env()`: ```python theme={"system"} DATE_FORMAT = env( "DJANGO_DATE_FORMAT", default="d M Y" ) ``` It's a good idea to provide a default value if your custom variable is missing from your DotEnv file. Finally, you can also modify Ghostwriter CLI to include your new variable. After building a new binary, you can set the value via Ghostwriter CLI's commands. # Overview Source: https://www.ghostwriter.wiki/development/stack-overview A review of Ghostwriter's technology stack ## The Ghostwriter Stack Ghostwriter is a web application written in Python 3.7 with the Django web framework. It is a collection of Python 3.7, HTML, JavaScript, CSS, Jinja, and Django code compartmentalized into multiple Django applications. This compartmentalization helps keep the code organized and easy to peruse during customization or development efforts. ### The Container — Docker Everything runs inside of a Docker container, with Docker Compose, for simple deployment and code updates. Logo Docker Documentation Ghostwriter uses the Docker **Python3.8-Alpine** image, a barebones image with Python 3.8 only the most necessary Python libraries. Docker Compose handles all of the dependencies, so nothing needs to be installed on the host except Docker. ### The Database — PostgreSQL The application uses a PostgreSQL backend that Django natively supports. However, should users desire to switch to a different type of backend, the Django `settings.py` can be updated to use SQLite, Oracle, or MySQL without any additional libraries. Django makes it easy to modify the Ghostwriter database models as well. Migrations are usually smooth and trouble-free, especially if you are customizing the models prior to using Ghostwriter in production. Trying to move code updates to a production server can be messy when migrations fail unexpectedly. This is a key reason Ghostwriter ships as a Docker container application. Docker makes it simpler to deploy a fresh server with existing data when code updates are ready to be deployed. ### Background Tasking — AQMP Finally, Django Q and Redis handle automated queue management processing (AQMP). Ghostwriter automates a number of things like updating domain categorization data and DNS records. These tasks are handed off to Redis for background processing. # Behind the Scenes Source: https://www.ghostwriter.wiki/development/stack-overview/behind-the-scenes A look at the thought process behind Ghostwriter's development This article walks through Ghostwriter's technology stack and discusses much of the thought process behind various design decisions that went into Ghostwriter's early development. Medium # Testing Code Source: https://www.ghostwriter.wiki/development/testing-code Developing and running unit tests ## Introducing Test Cases Ghostwriter follows Django's best practices and recommendations for unit testing. You can read more about Django unit tests here: Logo The project organizes unit tests by application. Each application (e.g., Rolodex, Shepherd) has a *tests* folder that contains scripts for testing forms, views, and models. Each type of test case includes a baseline of unit tests that are detailed below. ## Running Tests & Examining Coverage Tests are run through Django's *manage.py* and the `test` command. You can run all tests, a subset of tests, or individual tests. See below for examples: ```json theme={"system"} # Run all tests docker-compose -f local.yml run django coverage run manage.py test # Run only "Rolodex" tests docker-compose -f local.yml run django coverage run manage.py test ghostwriter.rolodex.tests # Run a specific test docker-compose -f local.yml run django coverage run manage.py test ghostwriter.rolodex.tests.test_models.ClientModelTests ``` A successful run of all unit tests may still display errors. Many of the unit tests intentionally trigger errors by passing invalid data to the server. The logger output can be disabled but this is generally unnecessary. A successful run will output something like this at the end: ```log theme={"system"} ---------------------------------------------------------------------- Ran 488 tests in 45.331s OK Destroying test database for alias 'default'... ``` A test run with failures or errors will report the number of each at the end. Review the test output to see which test(s) failed to determine what needs to be fixed. ### Test Coverage The above commands include the usage of Python's *coverage* library. Coverage compares the executed tests against the codebase to identify lines of code that were not tested. Logo A Coverage report can be generated once a test run is complete. This command will generate a command line report that displays the lines that were missed during testing: `docker-compose -f local.yml run django coverage report -m` A GitHub Action executes unit tests, generates an XML Coverage report, and uploads the report to CodeCov. This Action fires after commits to the `master` branch and whenever a PR is submitted to the repository. CodeCov tracks unit test coverage and makes it easier to view which folders, files, and individual lines of code need more unit testing. Logo # Access, Authentication, & Session Controls Source: https://www.ghostwriter.wiki/features/access-authentication-and-session-controls Everything that manages how users authenticate, what they can access, and how their web sessions work # Multi-Factor Authentication Source: https://www.ghostwriter.wiki/features/access-authentication-and-session-controls/multi-factor-authentication Using Ghostwriter's support for TOTP and WebAuthn MFA Ghostwriter v4+ supports multi-factor authentication (MFA) using both time-based one-time passwords (TOTP) and WebAuthn security keys/passkeys. ## Authenticator App (TOTP) To enroll a TOTP device, visit your account profile and click the **Set Up Authenticator App** button. Ghostwriter will provide you with a QR code to scan with 1Password, Google Authenticator, or any other TOTP app you prefer. The setup page also provides a secret if you want to configure an app manually. TOTP Setup ## Security Keys and Passkeys (WebAuthn) Ghostwriter also supports WebAuthn authentication using: * **Hardware security keys** (like YubiKey, Titan Security Key) * **Platform authenticators** (Touch ID, Face ID, Windows Hello) * **Passkeys** stored in password managers or devices To set up a security key: 1. Visit your account profile and click **Set Up Security Key** 2. Choose whether to create a **passkey** (for passwordless login) or a **security key** (for two-factor authentication) 3. Follow your browser's prompts to register your authenticator 4. Give your key a memorable name for easy identification Security Key Setup ### Passkey Login When passkey login is enabled, you'll see a **"Sign in with a passkey"** button on the login page. This allows you to authenticate using your registered passkey without entering a username and password. Administrators can control passkey login with the `DJANGO_MFA_PASSKEY_LOGIN_ENABLED` environment variable. It defaults to `true`. ### Managing Your Authentication Methods From your profile page, you can: * **Authenticator App**: Set up or remove TOTP authentication * **Security Key**: Add, edit, or remove WebAuthn devices * **Backup Codes**: Generate recovery codes for account access You can use both TOTP and WebAuthn methods simultaneously for maximum flexibility. ## Administrative Controls Administrators can configure accounts to require MFA before the user can access Ghostwriter. Requiring MFA is configurable on a per-user basis. With this option enabled, accounts without a valid MFA method can only log in, log out, change their password, and access the MFA setup pages. ## Backup and Recovery Ghostwriter's MFA supports backup codes that work with both TOTP and WebAuthn authentication. You can generate and retrieve your backup codes from your profile page at any time after configuring any MFA method. ## Browser Compatibility WebAuthn features require modern browsers that support the WebAuthn standard. Most current versions of Chrome, Firefox, Safari, and Edge support these features. Older browsers will fall back to traditional username/password and TOTP authentication. # Role-Based Access Controls Source: https://www.ghostwriter.wiki/features/access-authentication-and-session-controls/role-based-access-controls 1. [Features](/features) 2. [Access, Authentication, & Session Controls](/features/access-authentication-and-session-controls) Explanation of Ghostwriter's role-based access controls In Ghostwriter \<= v3.x, the role-based access controls described on this page apply *only to the GraphQL API*. In those older versions, user privileges are equivalent to every account having the `manager` role. ## Introduction User roles are the primary authorization mechanism. There are three user roles: * User * Manager * Admin A Ghostwriter administrator sets a user's role in the admin panel. All accounts are assigned the `user` role by default. If you look in the Hasura GraphQL console, you will also see a `public` role. Only Hasura uses this role. An unauthenticated request (i.e., any request that lacks a valid token in a request's `Authorization` header) is considered to have the `public` role. Specific webhook endpoints (e.g., `login`) are accessible to this role. The roles carry the following privileges: ### User Role Privileges The `user` role can only access project and related client data if they: * have been assigned to the project * have been invited to access the client * have been invited to access the project Otherwise, this role has the standard permissions you might expect. They can edit or delete their comments, update their profiles, and view the shared information in the various libraries (e.g., findings, domains). When viewing the project history for a domain or server, an account with the `user` role will only see project details if they can access the project. A `user` account can be granted special access to clients and projects via the invite system. Read below for details. #### Augmenting User Permission An admin can augment user permissions in the admin console. By default, the `user` role cannot edit, update, or delete entries in the shared Findings or Observation libraries. If you need to allow a user to maintain these records, grant them the necessary permissions in the admin console under their user record. The **Allow Report Template Management** augmentation permits a user to create, edit, protect, lint, and delete global report templates without granting the broader client and project visibility of the `manager` role. This is appropriate for roles such as technical writers who maintain shared report templates. This augmentation does not bypass client authorization. A template manager can manage a client-scoped template only if they already have access to that client. Users without this augmentation may continue creating and editing unprotected templates for clients they can access. ### Manager Role Privileges The `manager` role can view all clients and projects. The role can also: * invite others to access client data * invite others to access project data * assign others to a project * create and edit global report templates * protect report templates and edit templates flagged as *protected* * delete report templates If an account is flagged as a Django *Superuser* that account will automatically inherit the `manager` role. Users with thr `manager` role can invite users to have full access to a client or project from the client and project dashboards. ### Admin Role Privileges The `admin` role is only used by the GraphQL API. This role has complete access to everything available via the API. This role can create and manage users and modify fields not exposed to other roles. Use great care when assigning this role to an account. In general, a user should only ever be a `manager` or a `user`. ## Client and Project Invitations Someone with the `manager` or `admin` role can invite a user to access a client or project via the *Invitations* tabs on a client dashboard or the *People* tab on a project dashboard. Invitations help grant access to records without requiring the user be *assigned* as if they are working on the project. That means invitations are helpful tools for granting access to a client or project without altering the shape of the project team. Client invitations are powerful. A client invite grants access to that client and *all of that client's projects*. This is your shortcut for granting someone access to every project for a client, including any future projects. Project invitations grant access to the related client record and the target project, but not the client's other projects. It's very similar to a project assignment, but it does not change the shape of the project team. A project assignment and invitation have the same project-level boundary: the user can access the related client record and assigned project, but not the client's other projects (unless they also have a client invite or another project-specific grant). Invitations can be retracted at any time. The only impact is loss of access for the user. ### Invitation Case Study For example, if the *ClientA* has five projects, *Project1-5*, and a user is assigned to *Project5*, they cannot see *Project1-4*. If you want them to be able to review a previous project to prepare for this new project, you could invite them to *Project4*. That user would now be able to access *ClientA*, *Project4*, and *Project5*. If you needed them to be able to review all previous projects, a client invitation would be simpler than inviting the user to each individual project. However, it would also grant them access to any future projects under that client. That's when you may want to retract the client invitation at the end of the project. Client invites are also a good option for regular users who might be managing a specific client and need access to all current and future projects. # Session Management Source: https://www.ghostwriter.wiki/features/access-authentication-and-session-controls/session-management Managing user web sessions Ghostwriter v4+ provides options for managing user sessions. Previous Ghostwriter releases used Django’s defaults for session management. While these defaults are not inherently bad, they are permissive for applications storing sensitive information. There are several options an administrator can change to secure user sessions to their liking. ## Managing Session Expiry & Cookies The default Django sessions expire after two weeks. Ghostwriter tightens this by default, and administrators have control over three essential values: * `DJANGO_SESSION_COOKIE_AGE` * Sets the number of seconds a session cookie will last before expiring. The Ghostwriter CLI writes *32400* seconds, or nine hours, to the generated `.env` file. If the environment variable is missing, Django falls back to *7200* seconds, or two hours. * `DJANGO_SESSION_SAVE_EVERY_REQUEST` * Sets whether the session cookie will refresh on every request (default: *true*) * `DJANGO_SESSION_EXPIRE_AT_BROWSER_CLOSE` * Sets whether the session cookie will expire when the browser is closed (default: *true*) These defaults are a good starting point. Still, you should consider how your team uses Ghostwriter and adjust accordingly. The CLI-provisioned nine-hour value allows a login session to last an entire workday, while the application fallback keeps stale browser sessions shorter if the environment variable is not set. You may want to reduce this value to one or two hours for stricter session handling. With `DJANGO_SESSION_SAVE_EVERY_REQUEST` set to `true`, the server will update the session with each request. Updates reset the expiration, so a short expiry period won’t log out anyone actively using Ghostwriter but will allow inactive sessions to expire. If set to `true`, the last option will expire sessions after the browser quits. However, whether the session ends when you close the browser window depends on the browser. Some browsers, like Chrome, will keep sessions active, so you may need to quit or exit the browser to end the session versus just closing the browser window. You can manage these values via the Ghostwriter command-line interface (CLI) tool. ### Cleaning Up Expired Sessions Finally, administrators can view sessions in the admin panel under the *Sessions* section. This section records every session currently known to Ghostwriter, including expired sessions. If a user does not log out (e.g., lets their session expire) their session will remain logged in the database. Ghostwriter also tracks JWT sessions created by the GraphQL `login` mutation. These rows allow administrators to revoke login JWTs before their `exp` timestamp, but expired rows should still be cleaned up periodically. It is recommended you clear expired sessions on a regular basis to keep the session tables tidy. Use the `clear_expired_sessions` management command for this cleanup. The command wraps Django's built-in `clearsessions` command and also deletes expired GraphQL login sessions from Ghostwriter's `UserSession` table. For a scheduled task, select the allowlisted `ghostwriter.home.django_q_tasks.clear_expired_sessions` function. The wrapper accepts no arguments and runs only the `clear_expired_sessions` management command. Set up a task like this one with the cron scheduler. For example, `0 5 * * *` will run it every day at 5:00 AM. This is a custom command that runs Django's built-in `clearsessions` command to clear stale login sessions and also clears stale sessions for the GraphQL API. # Single Sign-On Source: https://www.ghostwriter.wiki/features/access-authentication-and-session-controls/single-sign-on Authentication and account creation via an external SSO provider Ghostwriter incorporates `django-allauth` to extend basic account creation and authentication to support Single Sign-On (SSO) and multi-factor authentication (MFA). You can learn more here: [Introduction - django-allauth](https://docs.allauth.org/en/latest/introduction/index.html) The `django-allauth` documentation covers the available SSO providers. There are dozens of options for social networks and business accounts, but the major providers you are probably looking for are covered–e.g., Microsoft, Google, GitHub, Slack, and Okta. ### Configuring an SSO Provider Once you have an SSO provider you want to implement, find the provider here to get the necessary configuration template: First, create a Python file inside the proper directory. For a default installation using the published Ghostwriter images, add files to *ghostwriter/settings* inside your operating system's data file directory. For `local-dev` or `local-prod` modes, create the file(s) in *config/settings/local.d* or *production.d* directory. Files in these directories are loaded after the main configuration files. They load in order, so you can create multiple files with number prefixes to control ordering (e.g., *1-custom-config.py* and *2-custom-config.py*). For example, you might make *1-sso-provider.py* to hold your SSO configuration and *2-mail-config.py* to hold your email backend configuration. Your new config file must contain these settings at a minimum. We'll use Microsoft as an example for these steps. ```python SSO Provider Config theme={"system"} # Provider(s) configuration SOCIALACCOUNT_PROVIDERS = { "microsoft": { "APP": { "client_id": "CLIENT ID", "secret": "SECRET", } }, } # Extend the installed apps with the SSO app for your provider(s) SSO_PROVIDERS = ["allauth.socialaccount.providers.microsoft"] INSTALLED_APPS = INSTALLED_APPS + SSO_PROVIDERS ``` The above lines add our provider (Microsoft for this example) to Ghostwriter's installed apps and provide the information necessary for the SSO handshake. You can find the values you need for your provider(s) at the above link. You can enable multiple SSO providers by extending the `SOCIALACCOUNT_PROVIDERS` configuration and list of `SSO_PROVIDERS`. There are a few more configuration options you may need to change. ### SSO Registration and Domain Allowlist When someone authenticates with an SSO provider, one of two things can happen: 1. The SSO login creates a new account linked to the provider. 2. The SSO login matches an existing local account; the two become linked, and the user is logged in under that local account. For the first scenario, you can control new account registration by toggling `DJANGO_SOCIAL_ACCOUNT_ALLOW_REGISTRATION` to `true` or `false` with Ghostwriter CLI. Alternatively, you can set `SOCIAL_ACCOUNT_ALLOW_REGISTRATION` in your config file. Using Ghostwriter CLI for these configuration changes makes everything easier. To apply the change (e.g., turning registration on or off), you must only bring the containers down and back up. If you change a Python config file, the containers must be rebuilt. You may want to allow registration but only for specific domains. The domain allowlist manages email domains you want to allow to authenticate or register via SSO. Like the registration setting, you can set this via Ghostwriter CLI or in your config file. Set `DJANGO_SOCIAL_ACCOUNT_DOMAIN_ALLOWLIST` with Ghostwriter CLI or `SOCIAL_ACCOUNT_DOMAIN_ALLOWLIST` in your config file. The allowlist can be defined as a space-separated list or a Python list (if setting it in your config file). Here is what this might look like in your config file: ```python SSO Domain Allowlist theme={"system"} # Enable or disable registration SOCIAL_ACCOUNT_ALLOW_REGISTRATION = True # Allow only these email domains SOCIAL_ACCOUNT_DOMAIN_ALLOWLIST = ["specterops.io"] ``` These settings allow registration via an SSO provider, but only if the account's email address has the *specterops.io* domain. If a local account with a matching email address already exists, the user will be prompted to enter a different address for their new account. This will most likely arise when transitioning from local accounts to a new SSO provider. You will probably want to link the accounts in these cases. An error is raised if multiple existing accounts share the same email address. Rather than trying to connect one of them, the user will see a message encouraging them to contact an administrator. You can link accounts by enabling your provider to authenticate via the account's email address. By default, email authentication is disabled, and the provider must have pre-verified the email address. Use the following settings if you trust the provider and want to consider email addresses as verified. ```python SSO Email Authentication theme={"system"} SOCIALACCOUNT_PROVIDERS = { "microsoft": { "APP": { "client_id": "", "secret": "", }, "EMAIL_AUTHENTICATION": True, "VERIFIED_EMAIL": True, }, } ``` These settings enable Microsoft email authentication and consider all email addresses verified for automatic connection. If someone were to authenticate with a Microsoft account, they would only be allowed to create or link an account if registration is enabled and the domain allowlist checks passed. If either check fails, the user will be redirected to a page like this. ### Additional SSO Settings Depending on your SSO provider, you may need to consider other configuration options. One common request is how to bypass clicking twice when signing in. By default, the sign-in page redirects users to a confirmation screen. The user must click the button to initiate the handshake with the SSO provider. This is a security feature to prevent abuse of an open redirect, but you can change the behavior. If you wish to have users log in immediately when they click the provider button, set `DJANGO_SOCIAL_ACCOUNT_LOGIN_ON_GET` to `true` with Ghostwriter CLI. For information is available here: docs.allauth.org # User Profile and Tokens Source: https://www.ghostwriter.wiki/features/access-authentication-and-session-controls/user-profile-and-tokens Managing account details, API tokens, and Service Tokens from the user profile The user profile page groups account management, profile details, and token management in one place. ## Profile Layout At the top of the profile page, the user's avatar links to the avatar update form. If you are viewing your own profile, the **Update Account Details** actions appear directly below the avatar. The account actions include: * Change Password * Update Personal Information * Manage Email(s) * Update Avatar * Manage MFA Settings The **Profile Overview** card presents user details in a condensed grid so they are easier to scan. Depending on whose profile you are viewing and your permissions, the card may include name, username, email, phone, timezone, role, access level, and groups. ## API Tokens The **API Tokens** card is for user-bound automation tokens. API tokens authenticate as your user account and inherit your current Ghostwriter permissions. Use API tokens when automation should do exactly what your user account can do. API tokens are opaque `gwat_` credentials. They are not JWTs, and Ghostwriter stores only a hash of the token secret. The full token value is shown once when you create or regenerate a token. Ghostwriter records an API token's last-used timestamp when the token authenticates. Updates are throttled to avoid writing on every request, so a recently active token may show the latest tracked interval instead of the exact most recent request time. API tokens are different from GraphQL `login` mutation sessions. The `login` mutation returns a short-lived JWT with a tracked session identifier so administrators can revoke active login sessions. See [Session Management](/features/access-authentication-and-session-controls/session-management) for login-session cleanup guidance. The API token card lets you: * create a new API token with an expiration date * review existing tokens * view token details, including current project access for the token's user * edit a token's expiration date * regenerate a token without changing its expiration date * revoke tokens you no longer need * hide expired tokens from the table Tokens expiring within seven days use the warning color. Expired tokens use the expired color. The **Hide Expired** preference is saved in your browser's local storage so the same browser remembers your choice. Token expiration changes follow the server's General Settings. New tokens and expiry edits cannot exceed the configured maximum lifetime, which defaults to 365 days from the time of the change. Existing tokens that already exceed a newly lowered maximum are not changed automatically, but they cannot be extended beyond the active policy. Shortening a token's expiry updates the existing credential in place. Extending a token's expiry either updates the existing credential or rotates it immediately, depending on whether administrators require rotation for expiry extensions. Expired tokens cannot be regenerated until their expiry is extended. If administrators do not require rotation for expiry extensions, regenerate the token after extending expiry to rotate the credential. ## Service Tokens The **Service Tokens** card is for non-human automation credentials. Service tokens authenticate as service principals and use only the permissions assigned to the token. They do not inherit the permissions of the user who created them. This separation is important: * A **Service Principal** is the durable non-human actor, such as an integration or automation service. * A **Service Token** is a credential that belongs to a service principal. * Permissions are assigned to the service token, not to the service principal. Use service tokens when automation should have a scoped set of permissions instead of all permissions held by a user account. Current service-token use cases include: * operation-log read/write tokens scoped to one operation log and its entries * project read-only tokens scoped to selected project data Service tokens use a shared GraphQL `service` role. The GraphQL schema can show queries, mutations, and Actions that are usable by other service-token presets, but the token's own grants still determine whether protected rows are returned and whether Django-backed Actions are allowed. An operation-log read/write token can therefore see project-read operations in the schema, but it cannot use them without a project-read grant. The service token card lets you: * create a new service token * choose or create a reusable service principal * select the token scope * review existing service tokens * regenerate a service token without changing its expiration date or scope * revoke tokens you no longer need * hide expired tokens from the table Service-token expiration styling and the **Hide Expired** browser preference work the same way as API tokens. Store newly created and regenerated API tokens and service tokens immediately. Ghostwriter only shows the token value once. Revocation and regeneration invalidate the previous credential immediately. # Overview Source: https://www.ghostwriter.wiki/features/background-tasks Configuring and scheduling background tasks Ghostwriter uses the [Django Q project](https://django-q.readthedocs.io/en/latest/) for queuing and managing background tasks. Django Q hands off tasks to the Redis server (already installed and running in Docker). Tasks are Python functions, usually defined in a `tasks.py` module. Ghostwriter executes some tasks on demand from the application. Only functions approved by the server-side Django Q policy can be added to a schedule in the admin panel. Tasks can be queued in a few different ways: * Schedule tasks to execute in the future and on a recurring schedule with Django Q. * Use the buttons (various) in Ghostwriter's web interface. * Use a REST API endpoint (not yet available). # Prebuilt Tasks Source: https://www.ghostwriter.wiki/features/background-tasks/prebuilt-tasks The following information is for Ghostwriter's provided tasks ## Release Domains The `ghostwriter.shepherd.tasks.release_domains` function checks if the currently checked-out domain names are due to be released. If Slack is enabled, it sends a Slack message when the domain's release date is tomorrow or today. If the release date is today, the domain is also released back into the pool. The task accepts an optional `no_action` argument that defaults to `False`. Set it to `True` to preview the domains due for release without changing their availability. Domains configured to reset their DNS records will use the Namecheap API during a normal release. DNS reset requires: * Namecheap API enabled and configured * The domain is registered with Namecheap * The domain's registrar in the domain library is set to "Namecheap" ## Release Servers The `ghostwriter.shepherd.tasks.release_servers` function checks if the currently checked-out servers are due to be released. It sends a Slack message if Slack is enabled and the server's release date is tomorrow or today. If the release date is today, the server is also released back into the pool. ## Check Domains The `ghostwriter.shepherd.tasks.check_domains` function checks each domain name to update categorization. The function uses the VirusTotal API to pull domain categorization data and related malware alerts. ## Update DNS The `ghostwriter.shepherd.tasks.update_dns` function updates Ghostwriter's records of each domain's current DNS records using `dnspython` and constructed DNS queries. ## Archive Projects The `ghostwriter.reporting.tasks.archive_projects` function collects a list of projects marked as complete and checks if the project's end date is 90 days (default) in the past. Completed projects older than the specified number of days are archived. This process mostly affects reports attached to the project. Each report is marked as complete (if not already marked as such) and marked as archived. All report types are generated and rolled into a zip file with copies of all of the evidence files. Finally, the evidence files are deleted. The archive files can be browsed and downloaded as needed. ## Scan Servers The `ghostwriter.shepherd.tasks.scan_servers` function collects a list of static servers cataloged in Ghostwriter, scans them for open ports using `python-nmap`, and records the results (the open port number and protocol). Then, the results are compared to previous results. Ghostwriter will send a Slack notification if a new port is open if Slack is enabled. This function focuses on the static servers because these servers are assumed to be owned by you and used for command and control (C2). These servers should not have open services exposed to the whole internet, so this is meant to alert you of open ports accessible outside of your management ranges. Transient servers (i.e., virtual private servers, cloud servers) will likely have open ports for phishing webpages and C2 redirection. If you will be using this task, the Q cluster needs to be started using an administrator / root permissions. Administrative privileges are required for the TCP SYN scan. ## Namecheap Synchronization The `ghostwriter.shepherd.tasks.fetch_namecheap_domains` task uses the Namecheap API to fetch all domains from the registrar for the configured account and synchronizes that information with the domain library. If a domain name is found in the library already, the task will update that record. If a Namecheap-registered domain in the Ghostwriter library is not found in the fresh list of domains, the task will mark that domain as expired. ## Cloud Monitoring The `ghostwriter.shepherd.tasks.review_cloud_infrastructure` task uses the Amazon Web Services and Digital Ocean APIs to check all running server instances and compare those to cloud servers attached to projects. If a project is marked as complete, the task will flag any running cloud servers attached to that project for review. The task will send a Slack message with all relevant server data to the project's channel if Slack is enabled. ## Update Expiration The `ghostwriter.shepherd.tasks.check_expiration` task checks the expiration dates of all domains in the domain library and compares them to the current date. If the domain is set to auto-renew, the task will increment the expiration date by one year. Otherwise, the task will mark the domain as expired. ## Operation Log Monitor The `ghostwriter.modules.oplog_monitors.review_active_logs` task checks operation logs for activity. It reviews all logs for all active projects. By default, the task looks for an entry created in the past 24 hours. You can change the hours by setting the `hours` argument when scheduling the task. The task outputs JSON detailing all activity logs that appear to be idle. This is useful for catching automated activity logging that may have started failing. If a Slack webhook is enabled and configured, the task will also send a Slack message to the project's Slack channel (if configured) or the global Slack channel. # Scheduling Tasks Source: https://www.ghostwriter.wiki/features/background-tasks/scheduled-tasks Configuring tasks to run repeatedly on a schedule ## Scheduling a Task Tasks are scheduled in the Django admin panel under *Django Q* and *Scheduled Tasks*. Add a new task, give the task a name, select one of the server-approved functions, and configure its schedule. Tasks can run once or repeatedly (e.g. minutes, hourly, daily, weekly). You can schedule them by time or using `cron`. The function and hook lists are controlled by a server-side allowlist. An administrator who only has access to the admin panel cannot add Python functions or system commands to these lists. You can provide approved arguments for functions that accept them. Ghostwriter rejects unknown arguments and values with the wrong type. For example, `ghostwriter.shepherd.tasks.scan_servers` accepts `only_active=True` to restrict scanning to servers that are in use. ### Example Scheduled Task Visit the Django Q database from the admin panel to access the *Scheduled Tasks*. For example, you may wish to create a scheduled task to automatically release domains at the end of a project. There is a task for this already in `tasks.py`, `ghostwriter.shepherd.tasks.release_domains`. It appears in the dropdown as *Release Domains*. ### Available Task Arguments Arguments may be supplied positionally in the *Arguments* field or by name in the *Keyword arguments* field. Both fields use Python literal syntax, so string values must be quoted. For example, enter `'nightly-backup'` in *Arguments* or `command_name='nightly-backup'` in *Keyword arguments*. Do not provide the same parameter in both fields. | Function | Available parameters | | -------------------------------------------------------- | ---------------------------------------------------------------------- | | `ghostwriter.reporting.tasks.archive_projects` | None | | `ghostwriter.rolodex.tasks.check_project_freshness` | None | | `ghostwriter.shepherd.tasks.check_domains` | `domain_id`: positive integer or `None`; omit it to check every domain | | `ghostwriter.shepherd.tasks.check_expiration` | None | | `ghostwriter.shepherd.tasks.fetch_namecheap_domains` | None | | `ghostwriter.shepherd.tasks.release_domains` | `no_action`: boolean; `True` previews without releasing | | `ghostwriter.shepherd.tasks.release_servers` | `no_action`: boolean; `True` previews without releasing | | `ghostwriter.shepherd.tasks.review_cloud_infrastructure` | `aws_only_running`: boolean; `do_only_running`: boolean | | `ghostwriter.shepherd.tasks.scan_servers` | `only_active`: boolean; `True` scans only servers in use | | `ghostwriter.shepherd.tasks.update_dns` | `domain`: positive integer or `None`; omit it to update every domain | | `ghostwriter.modules.oplog_monitors.review_active_logs` | `hours`: integer from 1 through 8,760; defaults to 24 | | `ghostwriter.home.django_q_tasks.clear_expired_sessions` | None | | Fixed system-command runner | `command_name`: one of the names in `GHOSTWRITER_DJANGO_Q_COMMANDS` | ## Configuring the Server Allowlist Ghostwriter includes its prebuilt scheduled tasks in the default policy. Server operators can replace or extend `GHOSTWRITER_DJANGO_Q_SCHEDULE_TASKS` from a settings fragment such as `settings/10-django-q-policy.py`. The Ghostwriter CLI mounts the `settings` directory read-only into the web and queue containers. ```python theme={"system"} GHOSTWRITER_DJANGO_Q_SCHEDULE_TASKS = { **GHOSTWRITER_DJANGO_Q_SCHEDULE_TASKS, "organization.tasks.refresh_inventory": { "label": "Refresh Organization Inventory", "args": [], "kwargs": { "active_only": {"type": "bool"}, }, }, } ``` Task entries use exact dotted callable paths. Prefixes and wildcards are not supported. Argument policies support `bool`, `float`, `int`, and `str` values, along with `name`, `required`, `required_parameters`, `nullable`, `choices`, `min`, and `max` restrictions. A named positional argument can also appear in `kwargs`, allowing administrators to use either input style. Set `allow_any_arguments` only for a trusted function that performs its own strict input validation. Result hooks use the separate `GHOSTWRITER_DJANGO_Q_SCHEDULE_HOOKS` mapping. Ghostwriter exposes its built-in `ghostwriter.modules.notifications_slack.send_slack_complete_msg` hook by default. Generic execution functions such as `os.system`, `subprocess.run`, and `django.core.management.call_command` should never be allowlisted. ### Fixed System Commands Server operators can expose a fixed command by adding it to `GHOSTWRITER_DJANGO_Q_COMMANDS`: ```python theme={"system"} GHOSTWRITER_DJANGO_Q_COMMANDS = { "nightly-backup": { "argv": ["/usr/local/bin/ghostwriter-backup", "--quiet"], "timeout": 1800, "cwd": "/app", "env": {}, }, } ``` The executable path and all arguments are fixed in the server configuration. Commands run without a shell and with only the explicitly configured environment. The admin panel permits selecting the command name but does not permit editing the executable, arguments, working directory, or environment. Restart both the Ghostwriter web and queue services after changing the policy. Changes are intentionally not loaded from the database or admin panel. ### Auditing Existing Schedules The task allowlist is deployment-specific, so Ghostwriter does not use a Django data migration to modify existing schedules. Instead, use the policy audit command as an operational migration when upgrading or enabling a more restrictive policy. For the safest rollout: 1. Stop the Django Q cluster so an old scheduler or worker cannot execute tasks during the upgrade. 2. Deploy the new Ghostwriter code and the intended server-side allowlist. 3. Audit the existing schedules: ```sh theme={"system"} python manage.py audit_django_q_policy --check ``` 4. Add any legitimate custom tasks reported by the audit to the server-side allowlist, or pause every schedule that does not satisfy the current policy: ```sh theme={"system"} python manage.py audit_django_q_policy --pause-disallowed ``` 5. Restart the Ghostwriter web and Django Q services. Disallowed schedules are paused by setting `repeats=0`; they are not deleted. This preserves their configuration for later review. If a disallowed schedule is missed by the audit, the restricted scheduler pauses it when it becomes due. A task already present in the queue is checked again by the worker and recorded as a failed task without importing or executing the denied function. Historical successful and failed task records remain available, but the admin panel cannot resubmit them unless they satisfy the current policy. The allowlist limits administrators who only control the application or Django admin panel. Anyone who can modify the server configuration, application code, queue, database, or application secrets is already inside the server trust boundary. # Overview Source: https://www.ghostwriter.wiki/features/bloodhound-integration-overview Connecting BloodHound and Ghostwriter for enhanced reporting You can connect Ghostwriter to your BloodHound instance(s) (Enterprise or Community Edition) to integrate BloodHound data directly into your reports. The BloodHound integration is compatible woth BloodHound v8.4 and later. # Accessing BloodHound Data in Reports Source: https://www.ghostwriter.wiki/features/bloodhound-integration/bloodhound-data-in-reports How to access the data in your report templates Accessing the data in your report template is simple. Everything is inside the `bloodhound` key in the report context. You can preview this by looking at the JSON report data. Inside the `bloodhound` key, you will find a `domains` key and, if it's BloodHound ENterprise data, a `findings` key. For example, you can iterate over the domains to create a table like so ```jinja2 theme={"system"} {%tr for domain in bloodhound.domains %} {{ domain.name }} {{ domain.distinguished_name }} {{ domain.functional_level }} {%tr endfor %} ``` ## Report Filters for BloodHound Data | **Filter** | **Usage** | | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `translate_domain_sid(SID, domains)` | Accepts the `bloodhound.domains` `dict` and a SID to translate the SID to a domain name. | | `filter_bhe_findings_by_domain(findings, SID)` | Accepts the `bloodhound.findings` `dict` and a SID to return only findings for that domain SID (based on the findings `environment_id`.) | # Configuring BloodHound Source: https://www.ghostwriter.wiki/features/bloodhound-integration/configuring-bloodhound How to integrate your BloodHound server(s) with Ghostwriter Ghostwriter integrates with BloodHound to pull data from the BloodHound graph and makes it available in your report templates. If the BloodHound API is enabled and configured, Ghostwriter pulls the BloodHound data when you generate a report. Ghostwriter integrates with v8.4 and later of *both* BloodHound Community Edition and BloodHound Enterprise. There are some differences between the two. You can learn more here: [Differences Between BHCE and BHE](/features/bloodhound-integration/differences-between-bhce-and-bhe) To enable the BloodHound API, generate your access token, and configure the API in Ghostwriter, follow these steps: ## Generating an Access Token Generate your BloodHound access token by logging into your BloodHound instance and opening the *Administration* section from the sidebar menu. Click *Manage Users* and then click the hamburger menu next to the user you want to use for the API access. Select *Generate/Revoke API Tokens*. Give your token a name and generate it. Take note of the *Key* and *ID* values in the modal window. ### Configuring the BloodHound API in Ghostwriter Take the *ID* and *Token* values and plug them into the BloodHound API Configuration in Ghostwriter. You will also need to add the URL of your BloodHound instance. The URL should be the base URL of your BloodHound instance, including the protocol and port (if needed). For example, `http://bloodhound:8080`. Ghostwriter supports two configuration models: * A shared global BloodHound configuration in the admin interface * A project-specific BloodHound configuration on an individual project The shared global configuration is reused by projects that do not have their own BloodHound API settings. Use the global configuration only when that shared-access model is intentional for your environment. By default, the global configuration is managed from the admin page only. Projects use it only when an admin enables *Allow Projects to Use Shared Configuration*. #### Testing the Configuration Once you have configured the BloodHound API in Ghostwriter, an admin can test the shared global configuration from the BloodHound admin page. This sends a request to the `/api/version` endpoint of the BloodHound API and verifies that the connection is successful. If a project has its own BloodHound API settings, project members can test that project-specific configuration from the project's *BloodHound* tab. #### Per-Project Instances If you have multiple BloodHound instances — such as seperate servers for different clients — you can configure a BloodHound server for a project. Project members can manage their project-specific BloodHound instance under the *BloodHound* tab on the project dashboard. If a project does not have its own BloodHound configuration, Ghostwriter falls back to the shared global configuration and uses the cached results associated with that shared server only when an admin has enabled project fallback on the global configuration. # Differences Between BHCE and BHE Source: https://www.ghostwriter.wiki/features/bloodhound-integration/differences-between-bhce-and-bhe Data differences between Community Edition and Enterprise Ghostwriter integrates with both BHCE and BHE to pull domain data from your server. The full set of information will vary based on the environment, but the data includes these items (and more) for each domain: * Domain name * User information — count, count of users with old passwords * Computer information — count, operating systems * Inbound and outbound trusts * Functional level * BloodHound's data quality assessment with totals for: * Organizational Units * Access Control Lists * Group Policy Objects * and more BHE's API offers a "findings" endpoint that is unavailable in BHCE. If the configured instance is a BHE instance, Ghostwriter will also collect the findings and make them available to reports. # Viewing & Managing BloodHound Data Source: https://www.ghostwriter.wiki/features/bloodhound-integration/managing-bloodhound-data How to view and manage your BloodHound data An overview of the BloodHound data is available under your Project dashboard. If you have a global server or project server configured, you will see the *BloodHound* tab on the dashboard. This tab will show you the configured server's URL and has buttons for testing the connection and fetching the latest data. When a project does not have its own BloodHound API settings, the dashboard falls back to the shared global BloodHound configuration only when an admin has enabled *Allow Projects to Use Shared Configuration*. In that case, the project view identifies that it is using the shared configuration and the cached results from that shared server. Once Ghostwriter has data, you will see one or more expandable sections. The *Domains* section shows an overview of the collected domains data. If the configured server is a BHE server, you will also see a *Findings* section with an overview of the BHE findings. ## Managing the Data Project members can refresh project-specific BloodHound data at any time by clicking the *Fetch BloodHound Data* button. That will kick-off the BloodHound connection that retrieves the latest data from the configured server. This process can take a few minutes to complete. A toast notification will appear to let you know the fetch was successful and instructing you to refresh the page to see the new data. The shared global BloodHound configuration is intended to be managed through the admin configuration page. If you use a shared global server, keep in mind that its configuration and cached results are reused across projects that rely on that fallback. # Overview Source: https://www.ghostwriter.wiki/features/client-and-project-management Managing clients and associated projects inside Ghostwriter Ghostwriter helps you track various information about clients and associated projects. The following sections break down the client and project dashboards. # Client Dashboard Source: https://www.ghostwriter.wiki/features/client-and-project-management/client-dashboard Walk through of the client dashboard The client dashboard is your go-to place for viewing everything about a specific client. The dashboard includes: * General Information–e.g., name, timezone * Points of Contact * Including names, timezones, and contact information * Project History * All projects–past and present–for the client * Infrastructure History * All servers and domains ever associated with work for the client * Extra Fields and Notes * Any additional information you want to track # Overview Source: https://www.ghostwriter.wiki/features/client-and-project-management/project-dashboard Walk through of the project dashboard The project dashboard is your go-to place for viewing everything about a specific project. It tracks all the essential information the assessment team needs to be effective. The *Planning* tab displays the basic information, like the assessment's duration, the teams' working hours (with time zone), and the project description. This tab also has the project calendar. The calendar automatically tracks team assignments, the assessment's duration, objective due dates, and the reporting period: The following few sections cover some of the specific tabs on the dashboard. For more information, see these sections as well: # Deconfliction Tracking Source: https://www.ghostwriter.wiki/features/client-and-project-management/project-dashboard/deconfliction-tracking Tracking deconfliction events during assessments When performing offensive assessment work, you will likely trigger an alert or generate anomalous logs that draw someone’s attention. If the system owner cannot identify you as the source, they will likely contact you to deconflict the event. You can record deconfliction events under a project’s *Deconflictions* tab. Each recorded event appears as a card, like so: Deconflictions are time-sensitive. Delayed or inconclusive responses can mean wasted effort and frustration for defenders. Events like these are why activity logging is critical. Each deconfliction card tracks a few key pieces of information and light metrics. Once you have responded, you can update the status to reflect if the event was or was not related to your work, and the card will show how much time has passed between receiving the deconfliction request and the final response. This data helps you and your client keep track of these events, but it can also reveal potential gaps and weaknesses in monitoring strategies. For example, suppose several hours have passed between the alert timestamp and the client contacting you. In that case, that could indicate defenders not receiving timely notifications or dealing with a lot of noise and a backlog of notifications. A careful review of deconfliction events before a post-assessment debrief call can offer interesting insights and topics of discussion. # Objective Tracking Source: https://www.ghostwriter.wiki/features/client-and-project-management/project-dashboard/objective-tracking Tracking the project's objectives, tasks, and status Objectives are key to any assessment. As such, they must be carefully tracked and well-understood by the team. The *Objective* tab helps the team track and prioritize their objectives and breaks them into sub-tasks. The objective list tracks each objective's status, completion progress, description, and sub-tasks. By clicking the status bar, the status rotates through several states: * **Active** – The objective is "live" and work can begin immediately. * **In Progress** – The team is currently working on the objective. * **Missed** – For objectives that cannot be completed. * **On Hold** – For objectives that have a pre-requisite or cannot be worked now. These status options can be customized in the admin panel if desired. # Project Points of Contact & Assignments Source: https://www.ghostwriter.wiki/features/client-and-project-management/project-dashboard/project-points-of-contact-and-assignments Tracking team assignments and project contacts The *People* tab shows the project's team assignments, roles, and any notes for the assignment. Since team members can be assigned multiple times during the project (during different non-overlapping periods), this tab and the project calendar are excellent references for who will join the project, when, and in what capacity. This tab also tracks the project's points of contact. Clients have points of contact, but not all client contacts may be relevant to a project. Under the *People* tab, you can add contacts from the client or create new project-specific contacts. You can mark one contact as the "primary" contact. This allows you to reference all points of contact or your project contacts in your reports. This is great for creating lists or tables of contacts in reports or adding the primary contact's name to a cover page. # Scope Tracking Source: https://www.ghostwriter.wiki/features/client-and-project-management/project-dashboard/scope-tracking Tracking hostnames and IP addresses in-scope for the project Offensive assessments need scope lists, which should be easy to reference. The *Scope* tab helps you track as many lists as you like and assign them different properties. Each scope list is a newline-separated list of strings (IP addresses or hostnames). Each list has a name, a preview of the first five lines, a button to expand it into a modal, a note field, and some properties displayed as icons next to the name. You can assign these properties: * Disallowed – The list includes hosts that should *not be* touched. * Requires Caution – The list contains hosts that require a "white glove" approach during testing. Clicking the *Expand* button opens a modal that displays the complete list. The *Copy to Clipboard* button copies the full list to your clipboard in a format suitable for standard tools (e.g., *Nmap*). Additionally, you can click the settings gear to access an *Export Text File* option. This option downloads a text file with the scope contents. The file can be imported using standard tools like *Nmap* or Burp Suite. # White Card Tracking Source: https://www.ghostwriter.wiki/features/client-and-project-management/project-dashboard/white-card-tracking Tracking white cards during assessments The project dashboard has a *White Cards* tab for tracking project white cards. Like the term [deconflict](/features/client-and-project-management/project-dashboard/deconfliction-tracking), *white cards* come to us from the U.S. military\_.\_ They refer to “a simulated event in an operational test.” A client may issue a white card for various reasons, such as if a system is too fragile or critical to risk attempting to exploit it or if there is a need or desire to bypass exploitation due to time constraints. The latter may be the most common white card. Today, we commonly refer to assessments with this white card as “assumed breach.” With such a white card, the assessment begins as if the team has successfully exploited an external system or gained access or credentials through other means (e.g., phishing). This white card and other simulated events must be documented and tracked. Each white card has a date and time the client issued it, a descriptive title or headline field, and a free-form field for more thorough or detailed descriptions. These fields make it simple to include these in a Ghostwriter report template as a list or table of white cards. # Overview Source: https://www.ghostwriter.wiki/features/findings-library Managing finding templates with the Finding Library The Findings Library lives at `/reporting/findings/`. The library is where users can view and edit finding templates for use in reports. Adding a finding to the library makes it available to all users. The library is intended to be the "source of truth" for your team when it comes to templates. Make sure everyone is aware that editing a finding in the library *changes it for everyone*. Edits for a specific project/report should be made after the finding is added to the report. Then edit the report's "local" copy. # Template Values for Findings Source: https://www.ghostwriter.wiki/features/findings-library/finding-keywords Using template values to format findings This markup language is under active development and will expand and change. This page will always have the most recent information. ## Introducing the Values Ghostwriter's reporting engine supports a few template values you can use in your findings templates to format text or insert data at reporting time dynamically. A reference pane is included at the top of the page when editing a finding in the library or a report. ### Using the Template Values While editing a finding, add the template values mid-sentence or on new lines. Certain values have specific requirements for placement, so read on to learn the basics. Type `@` to initiate auto-complete! Typing `@{` will display a list of all available template values. The curly brace matches the first character of the template variables, leading to the population of the autocomplete suggestions. Ghostwriter will process the values when a report is generated. To use a value, read its description for usage instructions and place the `{{.VALUE}}` keyword in your finding template. The `.` int `{{.VALUE}}` is important and easy to miss. This additional character is necessary to avoid processing other values inside curly braces as variables. Some people use `{{ }}` as a way to denote sections of text that should be filled in to use the template. ## Current Template Values The following table contains the current template values available for use in a finding: | **Keyword** | **Usage** | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `{{.client}}` | This keyword will be replaced with the client's short name. The full name will be used if a short name has not been set for the client. | | `{{.caption}}` | Start a line of text with this keyword to make it a caption. This is intended to follow a code block. | | `{{.caption reference}}` | Adding an alphanumeric string after `caption` (and a space) will place a bookmark that will link to the matching reference (created with the `{{.ref ...}}` tag). | | `{{.ref ...}}` | The "ref" tag places a bookmark that will link to the matching figure. Use evidence-friendly names or the reference you provide in a `{{.caption}}` tag. | Use the caption and reference tags to create cross-references in Word reports. For example, if you have an evidence image with the name "Screenshot" you can place the evidence (as shown below) using: `{{.Screenshot}}` Anywhere else in the finding (before or after `{{.Screenshot}}`) you can place one or more references to that image to create bookmarks. Like this: `{{.ref Screenshot}}` The final Word document will have your evidence with a "field character" (i.e., a reference like "Figure 1") and bookmarks (i.e., cross-references) to that field character. They will look and function just like cross-references inserted using Word's *Reference* tab. The same works with captions like this: `{{.caption myReference}}` and `{.ref myReference}}` ### Inserting Evidence Evidence files can be dynamically placed within a finding using the evidence file's **Friendly Name** value as a template value. **Example** An evidence file has been attached with the **Friendly Name** set to **Enigma**. The **Friendly Name** is a more human-friendly name (compared to the file path or a timestamped file name) for referencing the evidence file. When referencing evidence in a template, enclose the **Friendly Name** in the curly braces, e.g. `{{.Enigma}}`, on a new line by itself. There is no need for additional lines between the template value and the preceding or subsequent lines. Adding blank lines will just create blank lines in the report output. Let the formatting handle spacing between elements. With the evidence template value in place, Ghostwriter will drop in the evidence file in place of the template value when the report is generated. Additionally, Ghostwriter will add a border around the image and include the evidence file's **Report Caption** below the image as a proper Word caption. If the evidence is an image, the reporting engine will insert it as an image, centered, and set it to the width of the page (6.5"). You can change the sizing after the report is generated. If the evidence is a text document of some kind (e.g. log, txt, md) it will be placed in the document using the Word template's **Code Block** style. Edit your template to make adjustments to the font and other formatting options. The first time you open your Word report, you will see the Figures lack their numbers. This is caused by how Word parses the XML. The Figures are fine, but you need to tell Word to update them to see the numbering. Select all text in the report, right-click, and select **Update Field**. The Figures will now appear correctly. # Populating the Findings Library Source: https://www.ghostwriter.wiki/features/findings-library/populating-the-findings-library Adding findings to the library Templates can be added to the library one at a time or loaded en masse from a csv file. To add just one finding to the library, click the **Findings Library** tab on the menu bar and **Add New Finding**. This opens the finding form for documenting and submitting a single finding template. The **Finding Guidance** field is not used in reports. Use this field to explain how a finding should be used or what evidence should be included. The field can be left blank if you do not have anything for the finding. To bulk add templates to the library, click the **Findings Library** tab on the menu bar and **Upload Bulk Findings**. This opens the upload form for your csv file. The csv file must have these headers: *title, description, severity, impact, mitigation, replication\_steps, host\_detection\_techniques, network\_detection\_techniques, references, finding\_type, finding\_guidance* The *finding\_guidance* field is not used in reports. Use this field to explain how a finding should be used or what evidence should be included. The field can be left blank if you do not have anything for the finding. If a finding (based on the *title* field) already exists in the library, the import will update the existing record instead of discarding the data or duplicating the entry. # Authentication Source: https://www.ghostwriter.wiki/features/graphql-api/authentication Explanation of Ghostwriter's authentication tokens ## Introduction User login is handled with JSON Web Tokens (JWT). User-managed API tokens and service tokens are opaque database-backed credentials: * Submit credentials to the `login` action and receive a short-lived JWT that can interact with Ghostwriter as the authenticated user * Create an API token from your profile and receive an opaque `gwat_` token for user-bound automation * Create a service token from your profile and receive an opaque `gwst_` token for scoped non-human automation The JWT secret key is defined in the environment variables, `DJANGO_JWT_SECRET_KEY`. If you plug a Ghostwriter JWT into a debugger like the one at [https://jwt.io/](https://jwt.io/), you will see something similar to the following: ```bash HEADER theme={"system"} { "alg": "HS256", "typ": "ghostwriter-user+jwt" } ``` ```bash PAYLOAD theme={"system"} { "sub": "1", "sub_name": "benny", "sub_email": "benny@getghostwriter.io", "aud": "Ghostwriter", "iat": 1646088460, "exp": 1646117260, "jti": "4a838a9a-5f7c-43a0-b16d-1099fc951d54" } ``` ```bash SIGNATURE theme={"system"} SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ``` The tokens follow the JWT open standard ([RFC7519](https://datatracker.ietf.org/doc/html/rfc7519)). ### User Tokens User JWTs are generated with the `login` action. The resulting JWT holds the same privileges as the authenticated user. User JWTs are valid for 15 minutes and include a tracked session identifier so administrators can revoke active sessions. The collaborative editor uses JWTs with the `ghostwriter-collab+jwt` type. Hasura accepts these tokens only as the restricted `collab` role for the editor's required read-only GraphQL queries. General GraphQL Actions reject them except for the collaborative editor permission-check endpoint. Collab JWTs include scope claims for the editor model, object, report, and finding. The webhook exposes these claims as Hasura session variables so the `collab` role can read only the evidence rows needed by that editor page. Missing numeric IDs are represented as `-1`. Collab JWTs are not tracked in `UserSession`; that table is only for JWTs returned by the `login` action. ```json theme={"system"} mutation Login { login(password: "password", username: "username") { token expires } } ``` The `Login` action is disabled for accounts with MFA configured. An account with MFA should use a generated API token (see below). ### API Tokens You can also generate API tokens by visiting your profile page and using the **API Tokens** card. In this section, you can create new tokens, see all existing tokens, edit expiry dates, regenerate tokens, hide expired tokens, and revoke tokens you no longer need. API tokens are opaque tokens with a `gwat_` prefix. They are not JWTs and do not contain a readable payload. Ghostwriter stores a hash of the token secret and validates the database record on each request so tokens can be revoked immediately. Ghostwriter records a last-used timestamp for accepted API tokens, with throttling to avoid updating the database on every request. API tokens have user-defined expiration dates. They are intended for automation tasks that should inherit the current permissions of the user who created the token. General Settings control token lifecycle policy for both user tokens and service tokens. New tokens and expiry edits cannot exceed the configured maximum lifetime, which defaults to 365 days. Existing tokens that already exceed a lowered maximum are not shortened automatically, but they cannot be extended beyond the active policy. Shortening expiry updates the same credential in place. Extending expiry rotates the credential but administrators can disable forced rotation for expiry extensions. Regenerating or revoking a token invalidates the previous credential immediately. Expired tokens must have expiry extended before regeneration. If administrators do not require rotation for expiry extensions, regenerate the token after extending expiry to rotate the credential. ```plaintext Example Token theme={"system"} gwat_a0f02f188578a48b_ERJdqkkqz8U3Ny9cItiHT-1k62Ed4fNPPJ2LbjAVWH4 ``` ### Service Tokens Service tokens are created from the **Service Tokens** card on the user profile page. Service tokens are scoped credentials for non-human service principals. They do not inherit the permissions of the user who created them. Use service tokens when automation should have a fixed, limited permission set, such as: * Read/write access to one operation log and its entries * Read-only access to project data Service tokens share one Hasura `service` role. Hasura exposes queries, mutations, and Actions at the role level, so a service token might see GraphQL fields that are intended for another service-token preset. Token-specific permissions are still enforced by Hasura row filters and Django Action authorization. For example, an operation-log read/write token can see project-read Actions in the schema, but those Actions return an authorization error unless that specific token has the required project-read grant. These tokens are not JWTs. They are opaque tokens with a `gwst_` prefix for easy identification. Opaque tokens do not include a payload that describes a user or permission set. Rather, they are backed by a database entry that controls their permission set. Service tokens follow the same maximum lifetime, expiry-edit, regeneration, and revocation policy as API tokens. ```plaintext Example Token theme={"system"} gwst_32fabb637697aad2_OPWZ2iI9fkPYdbtkHOxBucdPfVG0NDaWDmMu5icCkLM ``` ## Token Authentication Requests authenticate with the `Authorization` header: `Authorization: Bearer TOKEN` Hasura connects to an authentication webhook before a request. The webhook accepts three credential families: * login JWTs returned by the `login` action * opaque user API tokens with the `gwat_` prefix * opaque service tokens with the `gwst_` prefix For login JWTs, the webhook takes several steps to examine the JWT before allowing a request to proceed: 1. Check the JWT is present 2. Attempt to decode the JWT and verify the signature, audience, and expiration 3. Verify the JWT type and required claims 4. Verify the tracked session exists, has not expired, has not been revoked, and belongs to the JWT subject 5. Finally, verify the user details are correct and that the account is still active If the token passes the above checks and your user's role is authorized (see [Authorization](/features/access-authentication-and-session-controls/role-based-access-controls)) to perform the query or mutation, you will receive a `200 OK` response with your requested data. For collaborative editor JWTs, the webhook verifies the JWT signature, audience, expiration, `ghostwriter-collab+jwt` type, and active user. It then returns the restricted `collab` role with the token's collab-scope session variables. The collaborative editor permission-check endpoint also requires the requested model and object ID to match the token's collab scope. For API tokens, the webhook validates the opaque token prefix and secret, checks the API token database record, and then authenticates the request as the token's user. Accepted API tokens update their last-used timestamp when it is missing or stale. For service tokens, the webhook validates the opaque token, verifies the service principal is active, and returns service-scoped session variables to Hasura. Service tokens do not become users and do not receive the permissions of the user who created them. The shared `service` role controls which schema fields are visible, while each token's `ServiceTokenPermission` rows control which protected rows and Django-backed Actions the token can use. If a request does not include an `Authorization` header, the webhook returns the `public` role with the username `anonymous`. This is not a real user or role and is only used to manage access to resources designed to be accessed without authentication. The only action available for this `anonymous` role is the `Login` action. If a request includes an invalid, expired, revoked, or otherwise unacceptable token, the authorization webhook will return a `401 Unauthorized` response with an error like this: ```json theme={"system"} { "errors": [ { "extensions": { "path": "$", "code": "access-denied" }, "message": "Authentication hook unauthorized this request" } ] } ``` ### Authentication & Authorization Flow This is a MermaidJS diagram showing the general flow for authentication and authorization: ```mermaid theme={"system"} sequenceDiagram autonumber actor User User->>GraphQL: Login Mutation loop Authentication GraphQL->>Webhook: Username & Password Webhook->>Back End: Who is this user? Back End->>Webhook: 'User' Object Webhook->>Back End: Create UserSession Webhook->>Webhook: Generate typed login JWT Webhook->>GraphQL: Login JWT end GraphQL->>User: Authentication Error || Pass login JWT to User User->>GraphQL: GraphQL Request with login JWT, gwat_ API token, or gwst_ service token loop Authorization GraphQL->>Webhook: Authorization Header alt Login JWT Webhook->>Webhook: Verify signature, audience, type, and exp Webhook->>Back End: Validate UserSession jti and active user Back End->>Webhook: User role session variables else API token Webhook->>Back End: Validate opaque gwat_ token record Back End->>Webhook: User role session variables else Service token Webhook->>Back End: Validate opaque gwst_ token, principal, and scope Back End->>Webhook: Service role session variables end Webhook->>GraphQL: Authentication Result end Note right of GraphQL: Check 'Authorization' header GraphQL->>User: Authorization Error || GraphQL Result ``` # Common API Actions Source: https://www.ghostwriter.wiki/features/graphql-api/common-api-actions Description and explanation of common API actions ## Getting to Know the API Schema The Hasura Console (see [Using the Hasura Console](/features/graphql-api/using-the-hasura-console)) is the easiest option for browsing the GraphQL schema. It's built into Ghostwriter, offers to autocomplete queries and mutations, and makes suggestions based on the schema, so it's friendly for people learning how GraphQL queries work. However, the Hasura Console should not be made widely accessible to anyone except the server administrator. For a safer option that offers all users access to something like the console, you can use an API testing/browsing application like Postman. Import the GraphQL SDL from the */DOCS/schema.graphql* in the code repository and configure your API token. For example, using Postman: 1. Click *New* and add a new API 2. Give it a name (e.g., Ghostwriter) and set the schema type to GraphQL 3. Save it and then open your newly created API 4. Click the *Definition* tab and paste in the contents of the */DOCS/schema.graphql* file 5. Click *Save* and wait a bit for Postman to process the definitions In the *Collections* tab, you can create requests with the newly created API. Select GraphQL as the format for the body and then select the API you just created from the dropdown list. Postman should auto-fetch the schema to enable auto-complete. Now you can explore the API and get comfortable before converting queries and mutations into code for automation. ### Basic Query and Mutations Most queries and mutations are straightforward and follow a naming convention that will become familiar. You can expect the following to be consistent: * To query for something, you need only to name it (e.g., `domain` to query domains) * To create a new record, you will use a mutation that begins with `insert_*` * To update an existing record, you will use a mutation that begins with `update_*` * To delete a record, you will use a mutation that begins with `delete_*` You will also notice copies of queries and mutations with the `*_by_pk` suffix. These are handy when acting against a singular entry and writing a simpler query. Instead of writing a `_where` clause that filters the results by the `id` you want, these queries and mutations need only an `id` argument. Both of these queries return the same result: ```json theme={"system"} query DomainByPk { domain_by_pk(id: 1) { name } } query Domain { domain(where: {id: {_eq: "1"}}) { name } } ``` ### Special Queries and Mutations Some queries and mutations have to break away from the above rules. These are special actions that trigger additional business logic before or after the action is completed. #### Domain and Server Checkouts The API uses `checkoutDomain` and `checkoutServer` mutations to prevent overlapping checkouts, attempts to checkout unavailable resources, and issues with bad dates. If a checkout passes all the checks, these actions also update the resource being checked out to mark them as unavailable. Likewise, there are matching `deleteDomainCheckout` and `deleteServerCheckout` mutations. As needed, these mutations set the domain or server back to the "available" status. #### Managing Evidence and Report Template Files If deleting evidence uploads or report template files via the API, you will want to use the `deleteEvidence` and `deleteTemplate` mutations. These actions delete the database records and the files stored on the server's filesystem. Use the `uploadReportTemplate` mutation when uploading a report template. The Action applies the same authorization rules as the Django interface: * A regular user must select a client they can access and cannot mark the template as protected. * Managers, administrators, and users granted **Allow Report Template Management** may upload global or protected templates. * Report template management permission does not grant access to unrelated clients. A client-scoped template remains limited by the authenticated user's existing client access. The `deleteTemplate` Action also verifies the authenticated user's report template management authority and access to the template's client scope. Although Hasura permits the `user` role to invoke the Action, an ordinary user without report template management permission cannot delete a template. #### Generating Reports You can use the `generateReport` mutation to get report data for a given report ID. The results offer the download URLs for docx, xlsx, and pptx. You can also request `reportData`, which is the raw JSON report data encoded as base64. ```json theme={"system"} mutation GenerateReport { generateReport(id: 1) { docxUrl pptxUrl xlsxUrl reportData } } ``` For more explicit examples and ideas, see this section: Explore various GraphQL usage examples. # Overview Source: https://www.ghostwriter.wiki/features/graphql-api/graphql-api ## Introduction Starting in v3.0.0, Ghostwriter includes a GraphQL API powered by the [Hasura GraphQL Engine](https://hasura.io/). You can use the API to perform all the same tasks available via the web interface. It's a powerful tool for automation, pushing and pulling data, and integrating external tools with Ghostwriter. ## Interacting with the API With the default configuration, the GraphQL endpoints are: * Local: [http://127.0.0.1:8080/v1/graphql](http://127.0.0.1:8000/graphql) * Production: [https://\/v1/graphql](http://127.0.0.1:8000/graphql) Unlike a REST API, a GraphQL API does not have specific endpoints you must query with a particular HTTP request type to receive a predetermined set of results. You submit queries with POST requests to one of the above endpoints as JSON. The JSON includes your personalized query and the data you selected to get back. You can get precisely what you need without making multiple requests or parsing extra data. A standard query is submitted with `Content-Type: application/json` in the headers and a body like this: ```json SampleQuery.json theme={"system"} { "query": "...", "operationName": "...", "variables": { "foo": "bar", ... } } ``` The `query` and `operationName` keys are required. The `operationName` tells GraphQL which action should be executed. This can be helpful if you stack multiple queries and mutations in the `query` key and want to execute them selectively (see the example at the bottom of the page). The response will always come back in this form: ```json SampleResponse.json theme={"system"} { "data": { ... }, "errors": [ ... ] } ``` For more information, review the GraphQL documentation on queries: Logo graphql Some basic GraphQL knowledge, such as the difference between a query and a mutation, will make the following sections easier to understand. You will also be better prepared to write your custom queries. ### Basic Queries A basic query looks like this: ```json SampleClientQuery.json theme={"system"} query MyQuery { client { id } } ``` It identifies itself as a query with an arbitrary name, states which table it wants to query, and what fields it wants to be returned. This query would return the `id` field of all client records accessible to the requesting user. The query can be modified to return additional data, like the `codename` and `note` fields: ```json SampleClientQuery.json theme={"system"} query MyQuery { client { id codename note } } ``` Field names are often placed on separate lines in GraphQL examples and the Hasura Console, but this is not required. You can separate field names with spaces, too. This option is easier to use when preparing queries for web requests because it removes the need to convert newlines to `\n`. Queries can also request related data. For a client, you might request the contact information for all related points of contact: ```json SampleClientQuery.json theme={"system"} query MyQuery { clients { id codename note contacts { email } } } ``` You can include multiple queries in a single request. Here we add a query to fetch the `id` and `title` of every finding in the database to get all our data back in a single request: ```json SampleClientQuery.json theme={"system"} query MyQuery { clients { id codename note contacts { email } } finding { id title } } ``` Finally, you might want to try to take the result of one query and use it as a variable for a subsequent query. When GraphQL receives multiple queries, like in the above example, GraphQL resolves all queries **simultaneously**, so the results of one cannot feed into another. In most cases, you can accomplish your goal with a single query. Always remember, you can leverage relationships. For this final example, assume you want to get the title and severity of every finding ever associated with a particular client's projects where the `title` contains `SMB`. This can be accomplished with nested database relationships and the addition of a condition: ```json SampleClientQueryWithFilter.json theme={"system"} query MyQuery { clients { projects { reports { reportedFindings(where: {title: {_like: "%SMB%"}}) { title severity { severity } } } } } } ``` Note how the above example references the `severity` relationship, instead of returning the findings `severityId` field. The `severityId` is just the foreign key, an integer. The query uses the relationship to return the string value set to represent that severity (e.g., High). ### Interacting via Automation Queries are simple until you need to pack them into the nested JSON for a web request. It would be best if you used a script to craft the proper payloads and make them repeatable. You can write your query in a human-readable format and then use something like JavaScript's `JSON.stringify()` or Python's `json.dumps()` to create the properly formatted payload for the POST request. However, this can lead to accidental double-encoding which will cause issues down the line. The simplest option is using a library built for handling GraphQL requests, like `gql` for Python. Here is an example query request in Python using the `gql` library. For more examples and ideas, see this section: Learn about GraphQL usage examples ```python SampleAPI.py theme={"system"} from gql import Client, gql from gql.transport.aiohttp import AIOHTTPTransport from gql.transport.exceptions import TransportQueryError from graphql.error.graphql_error import GraphQLError from asyncio.exceptions import TimeoutError try: # Define our queries and mutations as `gql()` objects login_query = gql( """ mutation Login { login(password:"sp3ct3rops", username:"benny") { token expires } } """ ) whoami_query = gql( """ query Whoami { whoami { username role expires } } """ ) # Prepare our initial unauthenticated GraphQL client transport = AIOHTTPTransport(url="http://127.0.0.1:8080/v1/graphql") client = Client(transport=transport, fetch_schema_from_transport=True) # Login and get our token result = client.execute(login_query) token = result["login"]["token"] # Setup future requests to use token headers = {"Authorization": f"Bearer {token}"} transport = AIOHTTPTransport(url="http://127.0.0.1:8080/v1/graphql", headers=headers) authenticated_client = Client(transport=transport, fetch_schema_from_transport=True) # Test the token with a `whois` query result = authenticated_client.execute(whoami_query) # Print our user information print(result) except TimeoutError: # Do something... pass except TransportQueryError as e: # Do something... pass except GraphQLError as e: # Do something... pass ``` # GraphQL Usage Examples Source: https://www.ghostwriter.wiki/features/graphql-api/graphql-usage-examples Examples of automating tasks and integrating external tools with Ghostwriter via the GraphQL API The following pages offer some ideas and examples for automating tasks or integrating tools with Ghostwriter for reporting or tracking infrastructure. Most examples will default to using Python's `gql` library, build upon the template you may have seen in the introduction to the GraphQL API, and assume you have an API token. This template is a good starting point for building automation or experimenting with the API. It includes logging and pulling information like your API token and Ghostwriter URL from a config file. ```bash ghostwriter_graphql.py theme={"system"} import configparser import logging from gql import Client, gql from gql.transport.aiohttp import AIOHTTPTransport from gql.transport.exceptions import TransportQueryError from graphql.error.graphql_error import GraphQLError from asyncio.exceptions import TimeoutError # Configure logging log_handler = logging.StreamHandler(sys.stdout) log_handler.setLevel(logging.DEBUG) log_format = logging.Formatter("%(levelname)s %(asctime)s %(message)s") log_handler.setFormatter(log_format) logger = logging.getLogger(__name__) logger.addHandler(log_handler) logger.setLevel(logging.INFO) # Load the config file values config = configparser.ConfigParser() config.read("config.ini") # Ghostwriter API URL and variables GHOSTWRITER_API_URL = f"{config['ghostwriter']['gw_url'].strip('/')}/v1/graphql" GHOSTWRITER_TOKEN = config['ghostwriter']['api_token'] try: # Define some queries or mutations as `gql()` objects here whoami_query = gql( """ query Whoami { whoami { username role expires } } """ ) # Configure the GQL transport headers = {"Authorization": f"Bearer {GHOSTWRITER_TOKEN}"} transport = AIOHTTPTransport(url=GHOSTWRITER_API_URL, headers=headers) authenticated_client = Client(transport=transport, fetch_schema_from_transport=True) # Test the token with a `whois` query result = authenticated_client.execute(whoami_query) logger.info(f"Authenticated as {result['whoami']['username']}") # Execute queries and mutation with `authenticated_client.execute()` here except TimeoutError: # Do something... pass except TransportQueryError as e: # Do something... pass except GraphQLError as e: # Do something... pass ``` Here is an example template for a config file you might use to store secrets and other variables: ```bash config.ini theme={"system"} [ghostwriter] gw_url=http://localhost:8080 project_id=11 api_token=eyJhbGciOiJIUzI1NiI... ``` You need three libraries to run the script: ```bash requirements.txt theme={"system"} requests==2.32.3 gql==3.5.0 aiohttp==3.10.5 ``` # Integrating with BloodHound for Reporting Source: https://www.ghostwriter.wiki/features/graphql-api/graphql-usage-examples/integrating-with-bloodhound-for-reporting Pass BloodHound data to Ghostwriter for inclusion in your reports BloodHound Community Edition (BHCE) is a part of many assessments, but using the data in reports can be difficult. The BHCE data is readily available as JSON, but the JSON files are typically large for most Active Directory (AD) environments outside of a lab environment. Also, the data’s full value comes from your analysis, so feeding the raw JSON to Ghostwriter isn’t the way to go. No one wants to copy and paste the contents of a dozen JSON files into fields anyway. We can leverage BHCE and Ghostwriter’s robust APIs to perform analysis, automatically pass the JSON to Ghostwriter, and store it in a JSON field. This example is covered more in-depth in this article: Logo Posts By SpecterOps Team Members A proof-of-concept script is available here: Logo A proof-of-concept script for automating the extraction of data from a BloodHound Community Edition server and sending it to Ghostwriter for use in reports # Recording Cloud Server Deployments Source: https://www.ghostwriter.wiki/features/graphql-api/graphql-usage-examples/recording-cloud-server-deployments Recording cloud server deployments in Ghostwriter with GraphQL You may have a tool, platform, or script that deploys servers for you. You can automatically record these deployments under your Ghostwriter project with a GraphQL query like this: ``` from datetime import datetime from typing import Any, Union, def record_server( activity_type_id: int, ip: str, name: str, project_id: int, role_id: int, provider_id: int, ) -> str: track_server_mutation = gql( """ mutation InsertCloudServer($activityTypeId: bigint, $auxAddress: [inet], $ipAddress: inet, $name: String, $note: String, $projectId: bigint, $serverRoleId: bigint, $serverProviderId: bigint) { insert_cloudServer_one(object: { activityTypeId: $activityTypeId, auxAddress: $auxAddress, ipAddress: $ipAddress, name: $name, note: $note, projectId: $projectId, serverRoleId: $serverRoleId, serverProviderId: $serverProviderId }) { id } } """ ) variables: dict[str, Union[int, str]] = { "activityTypeId": activity_type_id, "auxAddress": f"{{{ip}}}", "ipAddress": ip, "name": name, "description": f"Deployed at {datetime.utcnow().strftime('%F %H:%M:%S')} UTC", "projectId": project_id, "serverRoleId": role_id, "serverProviderId": provider_id, } res = authenticated_client.execute(track_server_mutation, variable_values=variables) return res ``` To get the correct ID values for activity, role, and provider, you can map those in your script or pull them at run-time using a GraphQL query. The easiest option is mapping the values, which are unlikely to change. ``` # Map values to IDs ACTIVITY_TYPE_ID_MAP: dict[str, int] = { "C2": 1, "Phishing": 2, } PROVIDER_ID_MAP: dict[str, int] = { "AWS": 1, "Azure": 2, "Digital Ocean": 3, "Google Compute Engine": 4, "Linode": 5, "Rackspace": 6, } ROLE_ID_MAP: dict[str, int] = { "C2": 1, "Redirector": 2, "Payload Hosting": 3, "SMTP": 4, "Burner Workstation": 5, } # Use the mapped values for the arguments ghostwriter_response: dict[str, Any] = record_server( activity_type_id=ACTIVITY_TYPE_ID_MAP.get("C2", 1), ip=ip, name=hostname, project_id=100, role_id=ROLE_ID_MAP.get("C2", 1), provider_id=PROVIDER_ID_MAP.get("AWS", 1), ) ``` # Using the Hasura Console Source: https://www.ghostwriter.wiki/features/graphql-api/using-the-hasura-console How to enable and use the Hasura Console to access the GraphQL API ## Introduction The Hasura GraphQL Engine offers a web console where you can explore the API. It includes a useful code exporter that can help you develop GraphQL queries and export them as JavaScript or TypeScript. The console is useful for crafting queries and experimenting with the API, but it can be dangerous. Hasura is connected **directly** to the PostgreSQL database! Changes made in the Hasura console take immediate effect. Changing the schema or deleting data will irreversibly change your database and could render Ghostwriter unusable. Further, Hasura will wipe any configuration changes when the service is restarted. Even so, some changes may trigger changes to the PostgreSQL database, resulting in mismatched configurations on restart. Hasura's configuration should be left alone unless you have read Hasura's documentation and are certain you know what you are doing. If accessed, the console should be used only for developing GraphQL queries. ## Accessing the Console Access to this console is disabled by default for new production installations. If you would like to access it, run these commands to enable the console and restart Ghostwriter: ``` ./ghostwriter-cli config set hasura_graphql_enable_console true ./ghostwriter-cli containers down ./ghostwriter-cli containers up ``` Once the services have restarted, the console will be available at: *https\://\/console* If you are running a local `dev` environment, the Hasura console is enabled and will run on port 8080. You can access the console by visiting: *[http://127.0.0.1:8080/console](http://127.0.0.1:8080/console)* Accessing the console requires the Hasura admin secret. You can get that by running: `./ghostwriter-cli config get hasura_password` ### Console Preparation When you first access the console, the *API Explorer* will be configured to use the admin secret with the `X-Hasura-Admin-Secret` header for authentication. While using this header, you will be acting as an administrator with more permissions than you will have as your user, and some actions will be unavailable because they require an API token. This can create confusion later if use your queries outside Hasura's console with your API token. It is best to create an API token for yourself by visiting your user profile and generating a new token or by using the `login` action. Once you have a token, uncheck the box next to the `-X-Hasura-Admin-Secret` header and create a new `Authorization` header. Your new *API Explorer* window will look something like this: Now you will see the GraphQL queries and mutations available to your user under the *Explorer* panel. # Health Monitoring Source: https://www.ghostwriter.wiki/features/health-monitoring How to monitor the health of Ghostwriter services ## Introducing Health Monitoring Ghostwriter monitors the health of its services in two ways: Docker health checks and internal monitoring and testing. ### Docker Health Checks Docker automatically monitors the containers via `HEALTHCHECK` commands (see [Docker documentation](https://docs.docker.com/engine/reference/builder/#healthcheck) for technical information). These commands check to make sure the service is responding and basic functionality is working. The results of these commands can be checked with this command: ```log theme={"system"} ./ghostwriter-cli running ``` Each container runs a service-specific command on a schedule. If the command returns successfully (exit code `0`), the `Status` column will show `healthy`. Any other exit code will flip the status to `unhealthy`, indicating the service is likely not functioning properly. By default, the commands run with these attributes that can be adjusted via Ghostwriter CLI's `config set` command: * Start running after 30s (`HEALTHCHECK_START`, default is `30s`) * Run every 120s (`HEALTHCHECK_INTERVAL`, default is `120s`) * Timeout after 10s (`HEALTHCHECK_TIMEOUT`, default is `10s`) * Will be retried once (`HEALTHCHECK_RETRIES`, default is `1`) ### Internal Testing and Monitoring Ghostwriter also tests each service more thoroughly with two API endpoints: * /status/ * /status/simple/ The first endpoint, */status*, tests critical services and displays a table of results: These tests are more thorough than the Docker health checks. For example, Docker will verify the database back end is listening and accepting connections, but Ghostwriter runs tests to ensure the database is accepting connections and reading and writing are working as expected. This endpoint can also return a JSON version of the test results if you set the `Accept: application/json` header. Running these tests constantly could be a strain on the server, so the tests run on-demand when you visit this page. You can visit the simplified endpoint, */status/simple/*, to run lightweight checks. This endpoint checks the web server, database status, and cache status and returns one of the following responses: | **Response** | **Response Code** | **Description** | | ------------ | ----------------- | -------------------------------------------------------------------------------- | | OK | 200 | System is healthy | | WARNING | 200 | One or more tests did not pass and you should check the detailed status endpoint | | ERROR | 500 | There was an unexpected error indicating a critical issue | The home dashboard displays a basic system health status. The status is based on the results from the simplified endpoint. ### Configuration The dashboard services map to the following configuration variables: * DiskUsage: `HEALTHCHECK_DISK_USAGE_MAX` (Default is `90` \[percentage]) * MemoryUsage: `HEALTHCHECK_MEM_MIN` (Default is `100` \[in MB]) You can configure these values manually in the `.env` file or with Ghostwriter CLI (this sets and reads from `.env`). For changes in `.env` or using the CLI, bring containers `down` and `up`. Example `.env` snippet with default values: ```sh theme={"system"} HEALTHCHECK_DISK_USAGE_MAX='90' HEALTHCHECK_MEM_MIN='100' ``` Example using the CLI to set and get the disk usage value: ```sh theme={"system"} ./ghostwriter-cli-linux config set HEALTHCHECK_DISK_USAGE_MAX 90 ./ghostwriter-cli-linux config get HEALTHCHECK_DISK_USAGE_MAX ``` ## Automated Monitoring You can automate monitoring with something like Uptime Kuma or a similar tool. The recommended logic is: * Request the */status/simple/* endpoint * Check for the response code and content * If `OK` is not in the response or the response code is not `200`, send an alert with a link to */status/* for details If the tool is capable of triggering a secondary action, you could have the tool pull the JSON data from */status/*: ```log theme={"system"} $ curl -k https://ghostwriter.local/status/ -H "Accept: application/json" {"Cache backend: default": "working", "DatabaseBackend": "working", "DefaultFileStorageHealthCheck": "working", "DiskUsage": "working", "MemoryUsage": "working", "MigrationsHealthCheck": "working", "RedisHealthCheck": "working"} ``` Working services will have a `working` status. A service experiencing a problem will have a descriptive warning or error message that will tell you why it failed the test. You can use this information to customize your monitoring alert. # Infrastructure Management Source: https://www.ghostwriter.wiki/features/infrastructure-management Using Ghostwriter to manage and monitor servers and domain names Ghostwriter helps you manage and monitor covert infrastructure, including servers and domain names. Tracking infrastructure in Ghostwriter creates a historical record of how and when your infrastructure was used. Additionally, the infrastructure manager can be setup to monitor assets for changes in domain categorization and open ports/services exposed to the public internet. # Domains Management Source: https://www.ghostwriter.wiki/features/infrastructure-management/domains-management Managing domains with the Domain Library ## The Domain Library The domain library lives at `/shepherd/domains/`. The library is where users can view the current status of each domain and check out a domain for a project. ## Adding Domains Domains can be added to the library one at a time or loaded en masse from a csv file. To add just one domain name to the library, click the **Domain Library** tab on the menu bar and **Add New Domain**. This opens the domain form for documenting and submitting a single domain name. You may not know the latest health information for the domain. You can set the domain to **Healthy** as a default value and leave categories blank. To bulk add servers to the library, visit the admin panel and navigate to the **Domains** model. Click the **Import** button and follow the on-screen instructions. You can upload csv, xls, xlsx, tsv, json, and yaml files. Select the matching format from the dropdown menu. After a moment, the admin panel will display a diff screen and ask you to approve the changes. If a domain name already exists in the library, the import will update the existing record instead of discarding the data or duplicating the entry. The DomainCheck tool outputs a csv in this format if you want to preload health statuses with your domains. You may then optionally add the `note` column. [https://github.com/GhostManager/DomainCheck](https://github.com/GhostManager/DomainCheck) # Domain Checkout Source: https://www.ghostwriter.wiki/features/infrastructure-management/domains-management/domain-checkout-1 Checking-out a domain for a project ## Checking-out a Domain Any **Available** domain in the library can be checked out for a project. Click the calendar icon in the **Checkout** column to bring up the check-out form. Each checkout requires selecting a client and a project. Select the client first to load the list of projects for that client. Then click the **Start Date** and **End Date** fields to open the datepicker and select your checkout window. The final step is filling in your usage information. Domains can be checked out for activities such as *Phishing* and *Command and Control*. Ghostwriter performs several operational security checks during the check-out process. The page will display a warning if: * someone previously used the domain name with the selected client * the domain name expires in less than 30 days and is not configured to auto-renew * the domain name is marked as burned / has an undesirable category # Monitoring Domains Source: https://www.ghostwriter.wiki/features/infrastructure-management/domains-management/monitoring-domains Performing health checkups on domain names ## Domain Health Checks Ghostwriter grades a domain's health as **Healthy** or **Burned**. Health is based on domain categorization and VirusTotal information. ### Categorization Health Domain categories are pulled from VirusTotal, which pulls categorization information from multiple sources. See the VirusTotal configuration for more information. Categorization data is stored as *jsonb* in the `categorization` field. The format is: ```bash theme={"system"} { "VENDOR": "CATEGORY", "VENDOR": "CATEGORY", ... } ``` This JSON data is displayed as a table under each domain's *Health* tab: Ghostwriter assumes these categories are bad, and any source flagging a domain with one of these categories will trigger the health status to flip to **Burned**: * spam * adult/mature content * extreme * gambling * hacking * malicious outbound data/botnets * malicious sources * malicious sources/malnets * malware repository * nudity * phishing * placeholders * pornography * potentially unwanted software * scam/questionable/illegal * spam * spyware and malware * suspicious * violence/hate/racism * weapons * web ads/analytic Most of these categories are self-explanatory, but some ⁠— like gambling ⁠— may not seem like they belong. * **Placeholders:** This often appears when a domain's category is undetermined. It translates to *Uncategorized* and may mean the domain is under review. * **Gambling:** Not malicious, but likely blocked in a corporate environment. If a domain is flagged as **Burned** it may still be recoverable. If you have a domain you like, it may be worth getting it recategorized and continuing to monitor its reputation to determine if it can be used after a cool-off period. ## Domain DNS Updates You can also track the current DNS records for your domain names. Ghostwriter pulls this information using DNS queries. These queries will not return subdomain records. You will have to manually track subdomains or use your registrar's API (if available) to pull these records. You can edit or add tasks to *tasks.py* to leverage an API. ## Queuing Domain Updates Scheduling these tasks will keep records up-to-date without requiring any user interaction. Domain update tasks exist in the `tasks.py`. These functions can be scheduled or requested manually. The **Domain Update Control Panel** lives at `/shepherd/update` and provides information on when the updates were last run, how long they took to complete, and their exit state (success or error messages). Click the **Start Update** button under the desired check to queue a check for *all domains*. To update domain information or DNS records for just a single domain, open the domain's details and expand the **Health and Categories** or **DNS Records** panes. Each of these panes contains a **Refresh** button. Click this button to queue an update for just the one domain. # Populating the Domain Library Source: https://www.ghostwriter.wiki/features/infrastructure-management/domains-management/populating-the-domain-library Adding domains to the library ## Adding Domains Domains can be added to the library one at a time or loaded en masse from a file. To add just one domain name to the library, click the **Domains** tab on the menu bar and **Add New Domain**. This opens the domain form for documenting and submitting a single domain name. You may not know the latest health information for the domain. You can set the domain to **Healthy** as a default value and leave categories blank. To bulk add servers to the library, visit the admin panel and navigate to the **Domains** model. Click the **Import** button and follow the on-screen instructions. You can upload csv, xls, xlsx, tsv, json, and yaml files. Select the matching format from the dropdown menu. After a moment, the admin panel will display a diff screen and ask you to approve the changes. If a domain already exists in the library, the import will update the existing record instead of discarding the data or duplicating the entry. # Server Management Source: https://www.ghostwriter.wiki/features/infrastructure-management/server-management Managing servers with the Server Library ## The Server Library The server library lives at `/shepherd/servers/`. The library is where users can view the current status of each server and checkout a server for a project. ### Types of Servers The infrastructure manager tracks static servers and transient servers. The server library tracks servers in the `StaticServer` model. These *static* servers are intended to be servers you own, such as your command control team servers. The `TransientServer` model tracks *transient* servers, the various cloud servers/virtual private servers that come and go during assessments. The **Add a Transient Server** button adds these servers to specific projects from the project details page. # Monitoring Servers Source: https://www.ghostwriter.wiki/features/infrastructure-management/server-management/monitoring-servers Performing health checkups on servers ## Server Health Checks Ghostwriter can monitor servers for open ports and alert you if a service is exposed to the internet. By default, the `tasks.scan_servers` function will scan all servers in the server library for open ports. For this to work properly the Ghostwriter server should **not** be in your management block, i.e. the IP block allowed to talk to your servers. If a server is found to be in use on an active assessment with an open port, the task will send a Slack notification (if enabled) to the project's channel. ### Scheduling Server Scans Use your own judgement to determine how best to handle scanning. You may wish to setup the task to run once an hour, or even more frequently. To increase speed and only scan servers in active use, set `only_active=True` in the kwargs. # Populating the Server Library Source: https://www.ghostwriter.wiki/features/infrastructure-management/server-management/populating-the-server-library Adding servers to the library ## Adding Servers Servers can be added to the library one at a time or loaded en masse from a csv file. To add just one server to the library, click the **Servers** tab on the menu bar and **Add New Server**. This opens the server form for documenting and submitting a single server. To bulk add servers to the library, visit the admin panel and navigate to the **Static Servers** model. Click the **Import** button and follow the on-screen instructions. You can upload csv, xls, xlsx, tsv, json, and yaml files. Select the matching format from the dropdown menu. After a moment, the admin panel will display a diff screen and ask you to approve the changes. If a server exists in the library, the import will update the existing record instead of discarding the data or duplicating the entry. ### Add Server Providers The infrastructure manager is seeded with a handful of popular server providers: * Linode * Rackspace * Digital Ocean * Microsoft Azure * Amazon Web Services * Google Compute Engine If your server provider is not pre-populated, new providers can be added via the Django admin panel, `/admin/shepherd/serverprovider/`. # Server Checkout Source: https://www.ghostwriter.wiki/features/infrastructure-management/server-management/server-management Checking-out a server for a project ## Checking-out a Server Any **Available** server in the library can be checked out for a project. Click the calendar icon in the **Checkout** column to bring up the check-out form. Each checkout requires selecting a client and a project. Select the client first to load the list of projects for that client. Then click the **Start Date** and **End Date** fields to open the datepicker and select your checkout window. The final step is filling in your usage information. Domains can be checked out for activities such as *Phishing* and *Command and Control*. # Observations Library Source: https://www.ghostwriter.wiki/features/observations-library Observations work like slimmed-down findings: they are items that can be added to a report and have their own library, but don't have as many built-in fields. They can be used to report other aspects about the system being tested; for example, things that the system does well. The library lives under `/reporting/observations/`. As with findings, observations created in the library are available to all users, and is intended to be a "single source of truth". As with findings, editing an observation in the library will also change it on all reports its attached to. If you need to edit the item for a single report, edit the attached observation from the report instead. # Overview Source: https://www.ghostwriter.wiki/features/operation-logs Using the Operation Logs The Ghostwriter operation logs are a place to view and edit all commands that have been executed for a particular project. With a simple table view, an operator or project manager can quickly review any action taken in a target network and supplement them with additional data. Commands can be imported and exported for use in external reporting tools. # Attaching Terminal Recordings to Log Entries Source: https://www.ghostwriter.wiki/features/operation-logs/attaching-terminal-recordings Document terminal activities with Asciinema recordings ## Overview Asciinema `.cast` files are text-based recordings of terminal sessions that capture every command, keystroke, and output from your terminal. By attaching Asciinema recordings to operation log entries, you create an immutable, reproducible record of your operational activities. ### Asciicast Files Asciinema records asciicast files. The file specification is detailed in the Asciinema documentation. Ghostwriter supports v2 and v3: * [asciicast v2](https://docs.asciinema.org/manual/asciicast/v2/) * [asciicast v3](https://docs.asciinema.org/manual/asciicast/v3/) Ghostwriter parses your uploaded asciicast file and stores the raw `i` and `o` (input and output) event strings alongside the recording file. In this way, Ghostwriter includes recording content in your searches when you filter your log entries. Ghostwriter supports both plain `.cast` files and Gzip-compressed `.cast.gz` files. Compressed recordings are recommended for long sessions or high-output operations to save storage space and bandwidth. Unlike manual notes or screenshots, terminal recordings provide full context: * **Complete command history** showing exactly what was run * **Full output** of all commands executed * **Timing information** showing when each command was issued * **Reproducible playback** allowing others to see the exact sequence of events ### When to Record Record and attach terminal activity when: * Running multi-step reconnaissance commands * Key exploitation or post-exploitation activities * System configuration changes * Capturing proof-of-concept demonstrations * Documenting complex technical workflows * Creating evidence for forensic analysis ### Prerequisites Asciinema files (`.cast` format) are plain-text JSON files that capture terminal sessions. Ghostwriter accepts both plain `.cast` files and Gzip-compressed `.cast.gz` files. You can create them using: * **Asciinema CLI**: `asciinema rec filename.cast` (creates while recording) * **Existing terminal session tools**: tmux/screen logs converted to `.cast` format * **Manual `.cast` file creation**: If you have terminal output in text format * **Compressed recordings**: Use `gzip filename.cast` to create `filename.cast.gz` for large recordings ## Recording a Terminal Session with Asciinema ### Installation If you don't have Asciinema installed, follow the instructions in [Asciinema's documentation](https://docs.asciinema.org/manual/cli/installation/). ```bash Mac theme={"system"} brew install asciinema ``` ```bash Linux (Debian/Ubuntu) theme={"system"} sudo apt-get install asciinema ``` ```bash Linux (Fedora/RHEL) theme={"system"} sudo dnf install asciinema ``` ```powershell Windows with Python theme={"system"} pip install asciinema ``` ### Recording a Session Start the recorder in your terminal: ```bash Start Recording theme={"system"} asciinema rec output-filename.cast ``` Execute your commands and activities as normal * Everything typed and all output is captured * The recording starts immediately and captures keystrokes in real-time End the recording by pressing `Ctrl+D` or typing `exit` * Asciinema will display upload options (decline if you want to keep it local) * The `.cast` file is saved to your specified filename Test playback to verify the recording: ```bash Playback Recording theme={"system"} asciinema play output-filename.cast ``` The `.cast` file format is a JSON-based plain-text file, approximately 10KB per minute of recording depending on output volume. For long sessions or high-output operations, compress the file using `gzip filename.cast` to create a much smaller `.cast.gz` file that Ghostwriter can use directly. ## Uploading Recordings via the Web Interface Once you have a `.cast` or `.cast.gz` file, you can attach it to a log entry: Open the log entry details by clicking the entry on left-side table view. The details will appear in the right-hand pane. Scroll to the **Terminal Recording** section If no recording has been attached yet, you will see the dropzone Drag and drop your `.cast` or `.cast.gz` file into the dropzone or click it to open the file browser Terminal recording dropzone The system will: * Validate the file extension is `.cast` or `.cast.gz` * For compressed files, automatically serve them with proper headers for browser decompression * Confirm the upload with a success message * Add the "recording" tag to the log entry * Make the recording available for playback ### Viewing and Playing Recordings Once a recording is attached, you can view it in the log entry details: Embedded Asciinema recording player in operation log entry Click the play button to watch the terminal session playback. ### Deleting or Replacing a Recording If a log entry already has a recording and you want to replace it: Open the log entry details by clicking the entry on left-side table view. The details will appear in the right-hand pane. Click the *Remove Recording* button to delete the recording. This action cannot be undone! Consider downloading a copy before removing the file. If you want to replace the recording, select a new `.cast` or `.cast.gz` file. ### Downloading a Recording To download a recording for archival or external analysis: Open the log entry details by clicking the entry on left-side table view. The details will appear in the right-hand pane. Click the *Download Recording* button below the recording. Some browsers may not prompt when downloading the Asciinema cast files. If it looks like nothing happened, check your downloads location for the file. Play locally using `asciinema play filename.cast` ## Uploading Recordings via GraphQL API If you're automating log entry creation, you can upload recordings programmatically: ### GraphQL Mutation: `uploadOplogRecording` ```graphql Upload a Recording theme={"system"} mutation UploadRecording { uploadOplogRecording(input: { oplogEntryId: 626 file_base64: "W1sxLjAsIEZhbHNlXSwgMC4xMjM0LCAidGV4dCIsICIkIl0=..." filename: "session.cast" }) { id } } ``` **Parameters:** * `oplogEntryId` (required): The ID of the log entry to attach the recording to * `file_base64` (required): The `.cast` or `.cast.gz` file encoded as base64 * `filename` (required): The filename of the recording (must end with `.cast` or `.cast.gz`) **Response:** ```json Upload Response theme={"system"} { "id": 456, } ``` **Authorization:** * Requires authentication * User must have edit permissions for the operation log entry's project ### Retrieving Recording Metadata and Content To download a recording or retrieve its details: ```graphql Download a Recording theme={"system"} query { downloadOplogRecording(input: { oplogEntryId: 626 }) { downloadUrl fileBase64 filename } } ``` **Response:** ```json Download Response theme={"system"} { "downloadUrl": "http://ghostwriter.local/oplog/recording/16/download", "fileBase64": "W1sxLjAsIEZhbHNlXSwgMC4xMjM0LCAidGV4dCIsICIkIl0=...", "filename": "session.cast" } ``` The `fileBase64` field is only populated if you explicitly request it in the query. Use `downloadUrl` for serving the file directly or to build download links. ## Asciinema Technical Details You can learn more about [Asciinema here](https://asciinema.org). ### `.cast` File Format The Asciinema `.cast` format is a new-line delimited JSON-based and follows this structure: ```json Example Asciinema File Header theme={"system"} {"version":3,"term":{"cols":88,"rows":25,"type":"xterm-256color","version":"iTerm2 3.6.9","theme":{"fg":"#f8f8f3","bg": "#212121","palette":"#21222b:#ec615c:#85f789:#f7cd7a:#8aa9f9:#bf94e5:#a1e7fb:#f8f8f3:#545454:#ee7773:#94fc9f:#f7cd7a: #d0aefa:#f197dc:#b9fdfe:#f8f8f3"}},"timestamp":1773250286,"env":{"SHELL":"/bin/zsh"}} [4.159, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"] ``` The file format is detailed in the [Asciinema documentation](https://docs.asciinema.org/manual/asciicast/v3/). ### File Size Considerations * Average recording: 10KB per minute for plain `.cast` files * High-output operations: Can reach 100+ KB per minute * Compressed `.cast.gz` files: Typically 10-20% of original size due to plaintext repetition * Asciinema does not support "lazy-loading" or streaming content, so the entire file must be loaded in browser memory for playback. For sessions longer than 30 minutes or operations with high terminal output (e.g., verbose enumeration tools, compilation logs), compress your `.cast` files using `gzip filename.cast` before uploading. This significantly reduces storage space and download time while maintaining full functionality in Ghostwriter's player. ## Best Practices * **Start recording before action begins**: Start Asciinema before running commands, not after * **Keep recordings focused**: Record a single activity or workflow, not entire operations * **Use meaningful filenames**: Name recordings by activity (e.g., `seatbelt-execution.cast`) * **Verify quality**: Test playback before attaching to ensure timing and output are correct * **Clean sensitive data**: If recording captures credentials or sensitive data, create a separate sanitized version * **Supplement with notes**: Use the log entry fields to describe the objective and key findings * **Compress cast files**: Compress recordings that are longer than 30 minutes or have a high volume of commands and/or output ## Troubleshooting **"Can't play the recording in Ghostwriter?"** Broken Asciinema playback * Files Asciinema cannot understand will appear as a broken video player * Verify the file is a valid `.cast` format from Asciinema * Check that the file extension is exactly `.cast` (lowercase) * Try playing it locally first: `asciinema play filename.cast` **"Recording seems to be missing commands?"** * Some command-line tools clear the screen; scroll through the playback timeline to find them * Check that the entire session was captured by playing the file locally first **"Can I edit a `.cast` file after recording?"** * Yes—`.cast` files are JSON and can be edited with any text editor * Common edits: removing sensitive commands, fixing timing issues, or adding annotations # Auditing Log Sanitization Source: https://www.ghostwriter.wiki/features/operation-logs/auditing-oplogs Auditing log sanitization status in the interface or the API Admins and managers may "sanitize" a log to keep the metadata and timeline while removing sensitive information like internal hostnames from the client network and command arguments (e.g., passwords, hashes, hostnames). This is often done as part of end-of-assessment clean up or after a fixed data retention period. That means it is a good practice to audit the logs to ensure they have been sanitized as required. Sanitization status can be visually checked by visiting the log's page. The sanitization badge will be in the top bar if the log has ever been sanitized. Clicking it will open a modal with information about when the log was sanitized, by who, and which fields they selected. Automations can query an activity log's `sanitizations` relationship to retrieve its audit history. Each result includes when the log was sanitized, who requested it, and the selected fields. The `updatedAt` value on an entry changes when its content changes, so an automation can determine whether the latest sanitization remains current by comparing the newest entry update with the newest sanitization: * A log is currently sanitized when it has a sanitization record and its newest entry `updatedAt` is earlier than or equal to that record's `sanitizedAt`. * A log needs review when it has no sanitization record, or its newest entry `updatedAt` is later than `sanitizedAt`. For example, query the latest values for one log: ```graphql theme={"system"} query OplogSanitizationStatus($oplogId: Int!) { oplog_by_pk(id: $oplogId) { sanitizations(limit: 1, order_by: {sanitizedAt: desc}) { sanitizedAt sanitizedByName fields } entries(limit: 1, order_by: {updatedAt: desc}) { updatedAt } } } ``` Sanitization records are read-only through GraphQL. Sanitizing entries remains an authorized, manual action in the web interface. # Interacting with the Operation Log Table Source: https://www.ghostwriter.wiki/features/operation-logs/create-a-new-entry Using the live activity log Once you are on the log entries page, you will be presented with an empty table. The following sections outline how to interact with the table and log entries. There will be times when you will need the log's unique ID. The ID number is displayed in the log header. ### Creating an Entry To manually create an entry, click on the "Create a new entry" button in the action bar or by pressing `CTRL+N`: You see a new row appear pre-populated with the current UTC timestamps and your username in the *Operator* field. When writing in a rich text field, type `@today` or `@date` followed by a space or punctuation to insert the current UTC date using Ghostwriter's configured date format. Type `@now` or `@time` the same way to insert the current time in UTC, such as `20:14:13 UTC`. ### Modifying an Entry You can modify fields by double-clicking the table row you want to edit or pressing *Enter* while it is selected. A modal form will open: You can also press *Enter* to submit your changes. Once you submit a change, the edits will sync via WebSockets and be visible to anyone with the log open. ### Linking, Copying, & Deleting Entries When you select a log entry, the right pane displays the entry's details. This detail pane includes several buttons. | Action | Description | | ------------ | ------------------------------------------------------------------------------------------ | | Edit | Opens the editing modal | | Copy | Creates a copy of the log entry (good for repeated tasks) | | Copy as JSON | Copies the entry's details as JSON (good for quickly passing activity details to a client) | | Link | Copy a deep-link to the log entry to your clipboard | | Delete | Deletes the log entry from the log | ### Managing the Table View The command bar at the top of the page includes several powerful customization features and your connection status indicator. #### Customizing Columns You can customize the left-hand table view using the *Show/Hide Columns* button. Click it to reveal the column options and toggle columns on or off. #### Sorting Entries You can also sort your log entries. By default, the page sorts log entries in descending order, from the most recent to the oldest. You can sort by any column by clicking it. You can sort by multiple columns by holding *Shift* as a you click. Your table column customizations and sorting will be saved in your browser's local storage, so it will persist between reloads. Reset table sorting by clicking the *Reset Sort* button. #### Filtering Entries You can filter the table using the box on the right. This filter helps you view log entries related to a specific user, host, or command. To use the filter, type in the keyword and press. The filter is applied as you type, so you can keep typing to narrow the results further. Note that text search will include columns you may have hidden and terminal recording content. If you're unsure why the filter returned certain log entries, check the entry's details for content from table-view columns you have hidden or recordings. #### Log Status and Connection The log header includes the WebSocket connection status. Since all entries are created, modified, and deleted using WebSockets, a persistent connection is maintained. If the connection is ever lost, the badge turns red and reports that it is disconnected. When disconnected, you will not be able to create, modify, or delete rows. When an authorized user has sanitized the log, the header also displays a **Sanitized** badge. Select it to see the last sanitization's date and time, requesting user, and selected fields. If an entry has been created or materially edited since that action, the badge indicates that there are new changes and the details explain that the sanitization is no longer current. ### Sanitizing Entries Managers and administrators can select **Sanitize Entries** from the overflow menu in the command bar. Sanitization is a manual, on-demand action; Ghostwriter does not automatically sanitize activity logs. Choose the fields to scrub and confirm the action. Sanitizing entries cannot be reversed. Export or otherwise preserve any information you need before running it. Ghostwriter records each successful sanitization with its date and time, the user who requested it, and the fields selected. The status uses the latest entry update time to help determine whether entry content has changed after the most recent sanitization. Updating tags on a log entry does not flag a sanitized log as needing review. You can safely update tags for reporting purposes after sanitization, if needed. ### Applying Tags to Log Entries Like many objects in Ghostwriter, you can add tags to a log entry to help with filtering and tracking. The log table will change how certain tags appear in the table: Tags that include: * `att&ck`, `attack`, `mitre`, or `ttp` will appear as red tags (e.g., `ttp:t1549`) * `creds` or `credentials` will appear as yellow tags * `vuln` will appear as green tags (e.g., `vulnerable:DotNetPE`) * `detect` will appear as blue tags (e.g., `detected`) * `objective` will appear as purple tags (e.g., `objective:1`) Additional styles may be added in the future for different tags. The development is open to suggestions. Ghostwriter automatically tags log entries that have multimedia content—e.g., linked evidence files and terminal recordings (more on that below). When you link multimedia files, Ghostwriter will add the `evidence` tag for linked evidence and the `recording` tag for terminal recordings. Ghostwriter will also remove the tags automatically if you remove the file(s). You can use tags to easily find log entries—like those with linked evidence or terminal recordings—by entering the tag into the filter box. ### Muting Log Notifications By default, all new operation logs have notifications enabled. The optional [Operation Log Monitor task](/features/background-tasks) handles notifications. If desired, a user with the `admin` or `manager` role can mute notifications from the hamburger menu in the upper-right corner of the logging page. Notification status is also displayed in the operation logs table: ## Attaching Content to Log Entries Beyond the core log entry fields, Ghostwriter supports attaching additional content to entries to provide more context and documentation. ### Attaching Evidence You can link existing report evidence to a log entry. This allows you to associate relevant information with the specific activity documented in the entry. Evidence is displayed in a dedicated section within the entry details, with direct links to the original evidence. See [Linking Evidence to Log Entries](/features/operation-logs/linking-evidence-to-entries) for detailed instructions. Your project must have an available report before you will be able to upload evidence or link evidence to a log entry. ### Attaching Terminal Recordings You can upload terminal session recordings (Asciinema `.cast` or `.cast.gz` files) to document the exact commands and output from your operational activities. Recordings are stored with the log entry and can be played back directly within the application using the integrated Asciinema player. See [Attaching Terminal Recordings to Log Entries](/features/operation-logs/attaching-terminal-recordings) for detailed instructions. # Creating a New Operation Log Source: https://www.ghostwriter.wiki/features/operation-logs/creating-a-new-oplog Creating a new log To create a new log, navigate to the *Operation Logs* section on the sidebar and select the *Add New Oplog* button. You will be prompted to enter a name and select the project to which this log belongs. While the name doesn't need to be unique to Ghostwriter, it does need to be unique to the project. Click *Submit* when you are done, and you will be taken to the log summary page. On the log summary page, click the link for the new log, and you will be taken to the log entries table. This is where the user can view, create, modify, and delete entries. # Exporting / Importing Operation Logs Source: https://www.ghostwriter.wiki/features/operation-logs/exporting-importing-oplogs Exporting and importing activity log data ### Exporting You can export a log to a csv file by navigating to the log list view and clicking the *Export* button next to the log you wish to export. ### Importing To import a CSV log file, open the *Operation Logs* menu in the sidebar and click *Import Oplog Entries*: Drag and drop your csv file or click the *Browse* button to select it. Once the file is selected, click on the green *Upload* button. You will be redirected to the log list page if successful. Otherwise, an error will be displayed indicating what went wrong during the import. # Generating Attack Path Narrative Outlines Source: https://www.ghostwriter.wiki/features/operation-logs/generating-narrative-outlines Build a narrative outline from operation log entries Ghostwriter can generate a narrative outline from an operation log and append it directly to a report's rich text field. This helps operators leverage log entries to kickstart their work on attack path narratives. Dialog for generating an attack path narrative outline from an operation log. When you click the outline button in a report field, Ghostwriter opens a small dialog that lets you choose which operation log to use. After you confirm, Ghostwriter appends one line per matching entry to the end of the field. Ghostwriter always includes entries tagged `report` or `evidence`. Administrators can also configure additional tags in **Global Report Configuration** with the **Outline Tags** setting. ## Configuring Narrative Outline Tags The **Outline Tags** setting accepts a comma-separated list of include rules: * Exact tag matches like `report`, `evidence`, or `credentials` * Wildcard prefix matches like `cred*` or `detect*` that match entries like `creds` or `detection` * Namespaced prefix shorthand like `att&ck:` Matching is case-insensitive, so `CREDENTIAL`, `credential`, and `Credential` are treated the same. ### Examples * `report,evidence,credential` includes entries tagged exactly with `report`, `evidence`, or `credential` * `report,evidence,cred*` includes `cred`, `creds`, `credential`, and `credentials` * `report,evidence,detect*` includes `detect` and `detection` * `report,evidence,att&ck:` includes any tag that starts with `att&ck:` Ghostwriter treats the list as include-only. An entry is included in the outline if it matches **any** configured rule. ## What the Outline Includes For each matching entry, Ghostwriter: * Sorts entries by start date and * Formats timestamps in UTC * Uses the default Ghostwriter date format for the date presentation * Falls back to `N/A` when key fields are blank * Appends linked evidence objects with references after the narrative line Because the generated content is appended to the field, users can click the button multiple times if needed. Ghostwriter does not remove previous outline content automatically. # Linking Evidence to Log Entries Source: https://www.ghostwriter.wiki/features/operation-logs/linking-evidence-to-entries Associate findings and artifacts with operation log activities ## Overview When documenting operational activities in a log entry, you may want to associate relevant evidence—such as screenshots or text documents— with that specific activity. Linking evidence to a log entry creates a traceable connection between your documented activity and the supporting materials under your report. ### When to Link Evidence Link evidence to a log entry when: * You performed a technique that resulted in a finding or vulnerability discovery * You captured a screenshot or artifact that represents the activity * You generated a proof-of-concept or supporting document during the logged activity * You want to maintain a clear connection between the activity timeline and the evidence collected Before you can upload evidence, your project must create a report. All evidence lives under a report. ## Linking Evidence with Log Entries There are two ways you can link evidence, the web interface and the GraphQL API. ### Using the Web Interface The simplest way to link evidence to a log entry is through the web interface. Begin by clicking your log entry to open the details views on the right. Scroll down to the dropzones to see the areas where you can upload evidence files or a terminal recording. Evidence linking dialog in operation log entry form You can attach a file here in three ways: * Click your log entry and paste into your browser to automatically attach an image file or screenshot from your clipboard * Drag and drop a file into the dropzone * Click the dropzone to open a file picker Once uploaded, the evidence will be linked and appear alongside the log entry's other details with the name of the file and who uploaded it. If you uploaded an image, you will also see a small preview. For text evidence, you can click the evidence to open the file's details and see a preview of the contents. Multiple pieces of evidence can be linked to a single log entry. Simply repeat the process to add additional evidence. ## Linking Evidence via GraphQL API If you're integrating Ghostwriter with an external system, you can link evidence to log entries using the GraphQL API: ```graphql Link Log Entry with Evidence theme={"system"} mutation LinkEvidence { linkOplogEvidence(input: { oplogEntryId: 626 evidenceId: 456 }) { id } } ``` **Parameters:** * `oplogEntryId` (required): The ID of the operation log entry * `evidenceId` (required): The ID of the evidence to link **Response:** ```json theme={"system"} { "id": 789, } ``` **Authorization:** * Requires authentication * User must have edit permissions for the operation log entry's project * The evidence must belong to the same project as the log entry **Error Scenarios:** * **Project Mismatch Error**: Evidence and log entry belong to different projects * **Not Found Error**: Entry or evidence ID does not exist * **Permission Denied Error**: User lacks edit permissions for the entry's project The GraphQL mutation is idempotent—linking the same evidence to the same entry multiple times is safe and will not create duplicate links. ### Evidence Tags When evidence is linked to a log entry, the entry automatically receives an `evidence` tag. This tag: * Appears in the log table for quick visual identification * Can be used for filtering and searching entries with linked evidence * Is automatically applied when the first evidence is linked * Is automatically removed when the last evidence is unlinked ## Viewing Linked Evidence If a piece of evidence is linked to at least one log entry, you will see a *Linked Log Entries* card at the bottom of the evidence file's details page. Display of linked evidence in operation log entry details This card contains the name of the related log and then smaller cards for each linked log entry under that log. The cards show the timestamp, name of the tool, and the name of the user who logged the activity. There is also a button that opens the deep-link to the log entry. This link will open the log and automatically scroll to and select the related log entry. ## Best Practices 1. **Link as you document**: When you capture evidence during an activity, link it to the corresponding log entry immediately while the context is fresh 2. **Use meaningful evidence names**: Give your evidence descriptive names so they're easy to identify during reporting 3. **Add captions**: Add your captions right away so you do not forget and provide context for your future self and others 4. **Keep entries and evidence in sync**: If you update an evidence item later, the linked entry will reference the current version 5. **Use tags for organization**: Combine evidence links with entry tags (e.g., `ttp:t1548`) for comprehensive activity documentation # Setting up Automated Logging Source: https://www.ghostwriter.wiki/features/operation-logs/setting-up-automated-logging Configuring the API endpoint for automatic activity logging Currently, two different C2 frameworks can easily integrate with Ghostwriter's GraphQL API: Mythic and Cobalt Strike. These utilities automatically create and update log entries. You can also write scripts to integrate other frameworks and tools. All you need to get started is an automation token. ## Obtaining an Automation Token For operation-log syncing, prefer a scoped service token when the integration supports it. A service token can be limited to one operation log and its entries, so the automation does not inherit all permissions from the user who created the token. Use an API token only when the automation should act as your user account and inherit your current permissions. For custom logging tools, you can also consider using the `login` action with the API, but generated API tokens or service tokens are usually a better fit for long-running automation. Read more about this process here: ## Setting up Syncing with Cobalt Strike Logo Standalone Cobalt Strike operation logging Aggressor script for Ghostwriter 2.0+ Clone the [cobalt\_sync project](https://github.com/GhostManager/cobalt_sync) to your Cobalt Strike team server and follow the instructions contained in the [README](https://github.com/GhostManager/cobalt_sync/blob/main/README.md) to enable syncing for each Cobalt Strike team server you deploy. **Note**: Cobalt Strike does not associate console output with the original command. Therefore, *cobalt\_sync* cannot automatically complete the output fields for log entries. Job IDs may be available for CObalt Strike in the future. ## Setting up Syncing with Mythic Logo Standalone Mythic C2 operation logging script for Ghostwriter v2.0+ Clone the [mythic\_sync project](https://github.com/GhostManager/mythic_sync) to your Mythic C2 server and follow the instructions contained in the README to enable syncing for each Mythic server you deploy. **Note**: Since Mythic associates output with the original command, the *mythic\_sync* project will retroactively update previous log entries when output is received. *This will overwrite any additional context added to the original entry within Ghostwriter before the new output was received.* # Reporting Source: https://www.ghostwriter.wiki/features/reporting Managing and using reporting options in Ghostwriter Ghostwriter manages all data related to clients, projects, infrastructure, and findings/observations. There is a tremendous amount of reporting potential for historical data and end of project work (e.g. client-facing reports). That means *reporting* is not limited to just rendering a report of findings from a project. More built-in statistics will come soon, but you can run reports using Django or a task in `tasks.py` (see [Background Tasks](/features/background-tasks)). # Overview Source: https://www.ghostwriter.wiki/features/reporting/collaborative-editing Introducing Ghostwriter's collaborative editor Ghostwriter uses an editor known as Tiptap to offer full collaborative editing for findings, project fields, and report fields. You’ve probably edited a Google Doc. In fact, you’ve probably edited a Google Doc at the same time as other people and enjoyed how you could see their edits appear on your screen in real-time. That is collaborative editing, and it’s a handy feature—not just because you can see what others are doing but also because you don’t have to worry about the changes you make overwriting anyone else when you save. Because Ghostwriter is an open-source project that manages sensitive data that shouldn’t be sent to untrusted parties, we needed a solution to maintain the infrastructure that was compatible with open-source, which ruled out many cloud-provided solutions. We wanted a solution that allowed us to extend it with custom elements for features such as embedding evidence. We also needed a solution that still allowed us to access the data on the server—not just in the web browser—so that Ghostwriter could still generate reports. And finally, we wanted something that is actively maintained and has an active userbase to ensure continued support. After searching, we concluded that the YJS ecosystem was the most mature for collaborative editing. YJS itself is a data format designed for the task—it tracks a log of operations so each peer can make its edits and publish updates to the document to other peers. The updates can be applied in any order, resulting in everyone having the same data after all of them are applied. Many text editors supporting YJS exist. We selected the Tiptap rich text editor due to its features, maturity, and extensibility with custom elements and formatting. We selected Tiptap’s Hocuspocus server for sharing collaborative edits between peers. This server exposes a WebSocket endpoint that the client publishes and receives updates on in real-time. It also handles loading and saving the YJS documents from a data store. The end result is a fully collaborative editing solution that is entirely self-contained within Ghostwriter. No use of cloud services and completely open-source. # Editor Features Source: https://www.ghostwriter.wiki/features/reporting/collaborative-editor/editor-features Features of the collaborative editor Collaborative editing is the star of the show, but the editor contains other features worth exploring. ## Inserting the Current Date and Time Type `@today` or its alias `@date` followed by a space or punctuation to insert the current UTC date using Ghostwriter's configured date format. For example, with the default format, `@today,` becomes `21 Jul 2026,`. Type `@now` or its alias `@time` the same way to insert the current time in UTC, such as `20:14:13 UTC`. The inserted values are ordinary text and do not change afterward. The shortcuts are disabled inside inline code and code blocks. ## Inserting Objects The editor supports inserting objects that make it easier to visualize certain elements of your writing, such as captions and evidence files. ### Inserting Evidence You can insert your uploaded evidence files by clicking the paperclip in the menu bar. A window will appear with options to select an existing piece of evidence from a list or upload a new piece of evidence. After inserting your evidence, a preview block will appear in the editor so you can see the evidence you have inserted and understand how it will look in line with your other content. ### Inserting Captions You can insert a caption by clicking the hamburger menu and then *Insert Caption*. The form will ask you to enter a reference name. A custom reference name is optional, but you will want one if you plan to create a reference/bookmark to your caption later. After inserting the caption, you will edit the text field to supply the contents of your caption. ## Passive Voice Checking The Ghostwriter image ships with a local copy of the English [spaCy](https://spacy.io) model, a natural language processor with the capability to detect passive voice. Editors often have to flag instances of passive voice when reviewing reports written in languages such as English. Language processing happens on demand. ### Performing a Passive Voice Check Click the hamburger menu and then *Check Passive Voice*. The local spaCy model will review the text and try to highlight any instances of passive voice. The model has about 95% accuracy. After highlighting instances of passive voice, you can clear the highlighting by editing that section. ### Changing the Language Ghostwriter ships with `en_core_web_sm`. You can change the language model using Ghostwriter CLI. SpaCy offers [trained pipelines for a couple dozen languages](https://spacy.io/models) and an option for a multi-language model. After you change the model, bring the containers down and back up. Ghostwriter will check if your desired model is downloaded at runtime. If the model is not downloaded, such as when you have just changed it, it will download the model. This adds a one-time delay of about 20-30 seconds to the service's start time. ```bash Linux theme={"system"} ./ghostwriter-cli config set spacy xx_ent_wiki_sm ./ghostwriter-cli down && ./ghostwriter-cli up ``` ```powershell Windows PowerShell theme={"system"} .\ghostwriter-cli config set spacy xx_ent_wiki_sm .\ghostwriter-cli down && .\ghostwriter-cli up ``` ```bash Mac theme={"system"} ./ghostwriter-cli config set spacy xx_ent_wiki_sm ./ghostwriter-cli down && ./ghostwriter-cli up ``` # Jinja2 Tips & Tricks Source: https://www.ghostwriter.wiki/features/reporting/jinja2-tips-and-tricks Examples of simple and advanced Jinja2 code for reporting This page includes some examples of Jinja2 code that might be useful for report templates. We will expand this page with simple and advanced examples based on community feedback. The code may or may not be ready to be dropped into a Word document. The examples may contain indents, newlines, placeholders, and comments to improve readability. You may wish to remove some of these elements before using the examples in your templates. ## Set a Unique ID for Each Finding This example sets up ID values for each severity category, loops over each finding, and sets a unique ID value based on the finding's severity. Jinja2 does not support indexing in the destination of a `set`block, so this particular use case requires some additional code. The code uses a namespace to track separate ID values for each severity category. As it loops over each finding, it increments the ID value by one for its severity and creates a finding ID string. The string is the finding's first tag, the first character of the severity, and the number value (e.g., `TAG-C-1`for the first critical finding). You can customize this example to change the ID string or start ID values at zero instead of one. ``` {% set ns = namespace(crit_id=0, high_id=0, med_id=0, low_id=0, info_id=0, finding_tag=0) %} {% for f in findings %} {% if f.severity == 'Critical' %} {% set ns.crit_id = ns.crit_id + 1 %} {% set ns.finding_tag = ns.crit_id %} {% endif %} {% if f.severity == 'High' %} {% set ns.high_id = ns.high_id + 1 %} {% set ns.finding_tag = ns.high_id %} {% endif %} {% if f.severity == 'Medium' %} {% set ns.med_id = ns.med_id + 1 %} {% set ns.finding_tag = ns.med_id %} {% endif %} {% if f.severity == 'Low' %} {% set ns.low_id = ns.low_id + 1 %} {% set ns.finding_tag = ns.low_id %} {% endif %} {% if f.severity == 'Info' %} {% set ns.info_id = ns.info_id + 1 %} {% set ns.finding_tag = ns.info_id %} {% endif %} {% set tag = tags|first() %} {% set sev = f.severity|first() %} {% set finding_id = "%s-%s-%s" % (tag,sev,ns.finding_tag) %} {{ finding_id }} {% endfor %} ``` # Report Templates Source: https://www.ghostwriter.wiki/features/reporting/report-templates Managing report templates for Word and PowerPoint ## Introducing Report Templates Ghostwriter builds Word and PowerPoint reports using templates. The project includes two basic templates: *template.docxs* and *template.pptx*. ### Report Template Library The template library is where you manage all of your templates. The library lives under `/reporting/templates/`. #### Uploading Templates Upload a template by selecting **Reports** from the sidebar and then clicking **Add New Report Template**. The template form has a few options for controlling how the template is used and who can edit it. Users with the default `user` role can upload and edit unprotected templates for clients they can access. They must select a client when uploading a template. Managers, administrators, and users granted **Allow Report Template Management** can also create global templates, mark templates as protected, edit global or protected templates, and delete templates. The report template management permission is useful for roles such as technical writers who maintain shared templates but should not have manager access. It does not grant access to additional clients or projects. A template manager can administer a client-scoped template only when they already have access to that client. The **Template Name** is what users will select from the report template dropdown menus. The **Doc Type** (*docx* or *pptx*) affects under which reports the template will appear as an option and how the template linter reads the document. These two fields are required. The template document is also required. Whether **Client** is required depends on the uploader's report template permissions, as described below. The remaining fields control the template's scope and behavior: * **Description** – Use to describe a template or note how it should be used * **Client** – A client-scoped template is only available when generating a report for a project linked to that client. Regular users must select a client they can access. Users with report template management permission may leave this blank to create a global template. * **Protected** – If checked, only managers, administrators, and users granted report template management permission can edit the template * **Change Log** – Use it to keep track of changes (pre-filled with the current date and an initial message when a template is first uploaded) Global templates are visible throughout Ghostwriter and may be selected for reports belonging to any client. Grant report template management permission only to users trusted to maintain these shared templates. ### How Ghostwriter Uses Templates When you generate a Word or PowerPoint document, Ghostwriter will follow this workflow: 1. Fetch your selected template OR the default template for that document type (docx or pptx) 2. Check the linter status of the template 3. If the linter status is acceptable (not `failed` or `error`), proceed with report generation Pay attention to the linter messages for your templates. You want to use templates that have successfully passed linting. A template with warnings is acceptable but the results may not be what you want. # Report Template Linting Source: https://www.ghostwriter.wiki/features/reporting/report-templates/report-template-linting Introducing the report template linter and how to read the results ## Introduction to the Report Template Linter Ghostwriter automatically lints report templates when: * The template is first created * The template file changes * The template's **Doc Type** value changes You can also request the template to be linted at any time by viewing the template's details and selecting **Lint** from the options menu. ## Template Statuses There are four possible linter results: * **Success** – The template is ready to be used * **Warning** – The template passed basic linting checks but might not give you the results you want * **Failed** – The template failed the basic linting checks and cannot be used for report generation * **Error** – Essentially the same as *Failed* but means the linter encountered an error and could not complete linting ### Linting Checks The linter checks several basic things to make sure the template is usable and then checks a few custom things: 1. \[All] Template file exists on the file system 2. \[All] File type matches the selected **Doc Type** value 3. \[All] File can be opened as the selected **Doc Type** value 4. \[PowerPoint Only] Template contains zero slides 5. \[Word Only] All Jinja2 expressions, statements, and filters are recognized 6. \[Word Only] Report engine can successfully render a document using the template 7. \[Word Only] Template contains the [recommended styles](/features/reporting/report-templates/word-template-styles) Any issues related to the first three checks or a test report generation will result in a **Failed** status. The rest of the checks will generate warnings. #### Reviewing Your Template's Styles If you'd like to check a Word template's available styles before uploading, open the template in Word and follow these steps: 1. Under the ribbon's *Home* tab, go to the styles gallery and locate the *Styles Pane* button 2. Click the button to open the pane and view the list of *Recommended* (default) styles 3. Look at the bottom of the pane to find the *List* filter dropdown box 4. Click the dropdown and select *In current document* The list of styles in a new document is far more limited than what you see in the style gallery or style pane. # Troubleshooting Word Templates Source: https://www.ghostwriter.wiki/features/reporting/report-templates/troubleshooting-word-templates Tips for troubleshooting templates reporting issues inside Word ## Potential Troubles You may see an error like this when opening your report in Word: Generally, clicking "Yes" here is fine, and Word will fix whatever error(s) it finds in the document. The trouble here is this could lead to Word removing or changing the content you want in the document. These changes may not be noticeable and cause you problems later. In some cases, the issue may be severe enough that Word displays this error after you click "Yes" or before ever displaying the above message: Read on to learn about the cause of these errors. If you'd rather try to fix the problem, jump down to [Fixing Errors](/features/reporting/report-templates/troubleshooting-word-templates#fixing-errors) below. ### Background Information Your Office documents are archive files with XML files. You can open them with a utility like 7-zip to see the contents. An average Word document looks like this: These XML files are OpenXML files. The *document.xml* file is where your report content lives, and the *styles.xml* file holds the styles saved in your document (these carry over from your template). As mentioned in the [Word Template Styles](/features/reporting/report-templates/word-template-styles) page, Word does not automatically include every style in a document. To minimize file size, only "built-in styles" and styles your document uses will travel with the document inside *styles.xml*. You can trigger one or both of the warnings shown above by following these steps: 1. Extract the contents of your Word document 2. Open *document.xml* and make an edit to invalidate the XML (e.g., delete an opening or closing element tag) 3. Save *document.xml* and re-compress the directory 4. Change the extension of your new *zip* file to *docx* and try opening it with Word As you change a document inside Word, the application is editing the XML. If Word detects a problem, it tries to fix it. This does not always go as planned. At times like these, you run into problems like whole sentences becoming part of a bookmark, paragraphs appearing as headers in your table of contents, or content disappearing. Ghostwriter opens your template file and uses that XML as a starting point to build your report. Next, Ghostwriter inserts the OpenXML for your report into *document.xml*. Sometimes, this may create invalid OpenXML. Usually, the root cause of the invalid XML is a problem in the template. For example, your template may have an issue in a list. Your template builds the list of items with a Jinja2 `for` loop, so the list in the template is a single item. If you add more bullets inside Word, the application will catch the issue and resolve it. The Jinja2 rendering happens outside of Word, so the issue is not caught and fixed before it creates a big enough issue that Word can't fix. ### Fixing Errors There is no easy solution for locating and fixing these errors. Thankfully, these errors are rare. Begin by narrowing down where the problem exists in your template. 1. Download and open your template 2. Delete half the document 3. Save the document 4. Upload the modified document to Ghostwriter 5. Generate your report with this modified template 6. Try opening the resulting report If the document opens, you now know the problem is located in the deleted section(s) of your template. If you see the same error, repeat the above steps. Once you know which deleted section contains your issue, you can repeat the process with that deleted section to zero in on the error's location. In general, sections containing lists and loops are sections to pay attention to. Once you know where the issue is, it is typically simple to fix the problem. You can delete the sentence, bullet, or other element and re-type it. That's it. Once done, save your template and upload the fixed version to Ghostwriter. # Word Template Styles Source: https://www.ghostwriter.wiki/features/reporting/report-templates/word-template-styles Using Ghostwriter's Word styles ## Ghostwriter's Styles You can configure many variables for a style in a Word document. In some cases, Ghostwriter must apply styles for you (ex: formatting text evidence in a finding). You can refine these styles by creating and editing these styles in your templates. ### Customizing the Styles These styles are called by name: | **Style Name** | **Description** | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `CodeBlock` | Style text evidence and anything in the WYSIWYG editor's code editor (must be a *Paragraph* style type). | | `CodeInline` | Style runs of text formatted as code in the WYSIWYG editor (must be a *Character* style type). | | `Number List` | Style numbered (ordered) lists. | | `Bullet List` | Style bulleted (unordered) lists. | | `Caption` | Built-in style used for captions below evidence and lines preceded by the *`{{.caption}}`* expression. | | `List Paragraph` | Built-in base style used for bulleted and numbered lists; the fallback built-in style if your template lacks customized styles. | | `BlockQuote` | Style used for block quotes. | | `Table Grid` | Built-in style used for tables. | | `Footnote Reference` | Built-in style used for footnote numbers. | | `Footnote Text` | Built-in style used for footnote text. | **Note on List Styles** You can choose not to create these list styles, but lists will probably not look right in your document. Word's built-in styles (e.g., *List Paragraph*) do not apply the proper/expected indentation. This leads to problems during later editing. When you create a list in Word, the application applies *List Paragraph* and additional styling depending on your selection (numbered or bulleted). The style will appear as *List Paragraph,DAI2* or similar. This style **does not exist** in your template until you use it once, so Ghostwriter can't default to using it (see below). Create a numbered list, open the styles tab, and save the style as a new style named *Numbered List*. Repeat this for bulleted lists. Feel free to modify the indentation for nested list items and any other style variables before you save your new style. **Note on Built-in Styles** Word offers many, many built-in styles you might expect to be available to Ghostwriter; however, these styles only exist in the Word *application*. Word will only add a style to your template's internal *styles.xml* when you use it to keep file size down. These styles have these attributes applied: ``. This means a style like *Caption* will not exist in your template until you've applied it or created it yourself. To add one of the built-in styles to your template, apply it once and save your document. You can undo the style. The important thing is the style appears under the *In current document* list in the Styles Pane. # Word Template Variables & Filters Source: https://www.ghostwriter.wiki/features/reporting/report-templates/word-template-variables Introducing the Word template variables ## Jinja2 Statements, Expressions, & Filters When you request a Word document, Ghostwriter opens your selected template file and processes any Jinja2 expressions within the document to create a new document. The new document is saved in memory and sent to you for download. Jinja2 uses *statements*, *expressions*, and *filters*. These equate to lines of code and variables: * **Statement** – `{% ... %}` * Statements are lines of code like `{% if some_variable %}` * **Expression** – `{{ ... }}` * In general, an expression works like a variable in most cases, like `{{ client.name }}` * **Filter** – `... |filter ...` * You can pipe a value into a filter to modify it, like `{{ client.name|title }}` Templates can contain basic expressions and more complicated statements (e.g., for loops, if/else). In addition to the custom expressions and filters documented on this page, Jinja2 offers built-in statements, expressions, and filters you can use with Ghostwriter templates. To prevent cross-site scripting (XSS), Ghostwriter sanitizes formatted text fields. This sanitization creates a minor conflict with Jinja2 because it will escape `<` and `>` (e.g., replace the character with `%gt;`). If you want to check if something is greater or less than a value, use Jinja2's `gt()` and `lt()` tests. [https://jinja.palletsprojects.com/en/stable/templates/#jinja-tests.gt](https://jinja.palletsprojects.com/en/stable/templates/#jinja-tests.gt) [https://jinja.palletsprojects.com/en/stable/templates/#jinja-tests.lt](https://jinja.palletsprojects.com/en/stable/templates/#jinja-tests.lt) You can freely use `<` or `>` if you use it inside your report template (not a text field inside Ghostwriter). The official Jinja2 documentation contains all the information you need to get started using its more advanced features. There are also various considerations covered in the official Jinja2 documentation, such as whitespace control and escaping. [Template Designer Documentation](https://jinja.palletsprojects.com/en/2.11.x/templates/) All of Ghostwriter's expressions and statements should be wrapped in curly braces with one space to either side (`{{ client.name }}`or `{% if ... %}` ) – unless otherwise noted. If you do not include the spaces, Jinja2 will not recognize the expression as valid and will ignore it. If you ever need to include double curly braces or Jinja2 code inside a report and you **do not** want it to be rendered, you can escape your text in a couple of ways. One option is the `{% raw %}{% endraw %}` block. Another is using Jinja2 's "literal variable delimiter" (`{{`) inside a variable expression (e.g., `{{ '{{' }}`). [https://jinja.palletsprojects.com/en/3.0.x/templates/#escaping](https://jinja.palletsprojects.com/en/3.0.x/templates/#escaping) ### Using Conditionals One of the easiest and most powerful things you can do in your templates is leverage conditional statements to control content. For example, you can use an `if` block to check a value to determine the content or formatting. Conditional blocks are powerful when combined with things like [your custom extra fields](/configuring-global-settings/configuring-extra-fields). Conditional blocks can be written in a couple of different ways. The simplest will be familiar to anyone who has written a script in a language like Python: ``` {% if SomeCondition %} {% else %} {% endif %} ``` This approach is easy to read as is, but can be very messy and difficult to follow. Written on several lines like this will cause erroneous blank lines in the final document. You want to remove the newlines before finalizing your template for the best outcome. ``` {% if SomeCondition %}{% else %}{% endif %} ``` You can see how this version could become difficult to read. You can often condense your conditional down into a simpler version. For example, let's say you are looping over your project objectives for a table and want a cell colored green if the objective is complete or red if not. This single line handles that formatting: ``` {% cellbg "A8D08D" if obj.complete else "FF7E79" %} ``` More on `cellbg`, creating tables, and other functionality below! ### Potentially Useful Jinja2 Expressions These expressions are built into Jinja2 and might be helpful in your Word documents: | **Expression** | **Description** | | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `capitalize(string)` | Capitalize the first character and convert the rest to lowercase | | `lower(string)` | Convert a value to all lowercase | | `replace(string, old, new)` | Replace the old string (a substring of the first argument) with a new string | | `title(string)` | Return a titlecased string | | `trim(value, chars=None)` | Strip leading and trailing characters (default is whitespace) | | `unique(value, case_sensitive=False)` | Return a list of unique items from an iterable | | `upper(string)` | Convert a value to all uppercase | | `sort(iterable, reverse=False, case_sensitive=False, attribute=None)` | Sort an iterable with Python's `sorted()`. [More info](https://jinja.palletsprojects.com/en/3.0.x/templates/#jinja-filters.sort) | | `dictsort(mapping, case_sensitive=False, by="key", reverse=False)` | Like `sort` but accepts a mapping of key and value pairs to yield a dictionary. [More info](https://jinja.palletsprojects.com/en/3.0.x/templates/#jinja-filters.dictsort) | There are many other expressions and filters available. If you want to do something, there is probably a way to accomplish it with a built-in expression cleanly. You can perform math, logic, string mutations, and more. Check the Jinja2 documentation: [https://jinja.palletsprojects.com/en/3.1.x/templates/#expressions](https://jinja.palletsprojects.com/en/3.1.x/templates/#expressions) ### Ghostwriter Expressions To see what is available for your report, generate the JSON report. Everything in the resulting JSON will be available in a report template. The following table describes the top-level keys: | **Expression** | **Description** | | ---------------- | --------------------------------------------------------------------------------------------------------------------- | | `report_date` | \[`String`] Full date the report was generated (localized based on server settings) | | `project` | \[`Dict`] All information about the project | | `client` | \[`Dict`] All information about the project's client | | `team` | \[`Dict`] All team information (individuals assigned to the project) | | `objectives` | \[`Dict`] All objectives information | | `targets` | \[`Dict`] All project targets | | `scope` | \[`Dict`] All project scope lists | | `bloodhound` | \[`Dict`] All BloodHound information if a BloodHound server is configured and data is available | | `infrastructure` | \[`Dict`] All project infrastructure information | | `logs` | \[`Dict`] All activity logs and related entries from the project | | `findings` | \[`Dict`] All information about a project's findings | | `observations` | \[`Dict`] All information about a project's observations | | `docx_template` | \[`Dict`] All information about the selected DOCX template | | `pptx_template` | \[`Dict`] All information about the selected PPTX template | | `company` | \[`Dict`] All information about your company (configured in the admin panel) | | `title` | \[`String`] The report's title set in Ghostwriter | | `complete` | \[`Bool`] Value indicating if the report has been marked as complete | | `archived` | \[`Bool`] Value indicating if the report has been marked as archived | | `delivered` | \[`Bool`] Value indicating if the report has been marked as delivered | | `totals` | \[`Dict`] Various sums and counts of different project-related values (e.g., total findings, objectives, and targets) | Dates are localized based on your locale configuration in the server settings. The default date format is *M d, Y* (e.g., June 22, 2021). The `project` key has separate values for the day, month, and year the project started and ended. Use these to assemble your own date or date range formats if you need to represent a date differently or only want part of the date. If you do not have a client `short_name` value set, Ghostwriter will replace references to `client.short_name` with the client's full name. #### Findings Attributes – HTML & Rich Text Attributes You write your findings in Ghostwriter's WYSIWYG editor, where you can style text as you would directly in Word. The WYSIWYG editor uses HTML, so Ghostwriter stores your content as HTML. Let's say you put the following Jinja2 code in a template: ``` {% for finding in findings %} {{ finding.description }} {% endfor %} ``` That would drop in raw HTML using whatever style you had assigned to `{{ finding.description }}` in the template. It's unlikely you would want that. Jinja2's `striptags` filter can help, but it removes all HTML without preserving new lines. Ghostwriter's custom `strip_html` filter will strip the tags and preserve newlines, but the output will still be all plaintext. You must re-apply character and paragraph styles, font changes, and other options. Your evidence files will also appear as text placeholders. To get what you see in the WYSIWYG editor in your Word document, add `_rt` (for rich text) to the attribute's name, use the `p` tag (see **Ghostwriter Tags** below). The above example becomes: ``` {% for finding in findings %} {{p finding.description_rt }} {% endfor %} ``` This will drop in your WYSIWYG HTML converted to Open XML for Word. Your image and text evidence will be present (with style and border options applied), and your text will be styled. Each finding also has a unique `severity_rt` attribute. You don't style this text in the WYSIWYG editor. Ghostwriter creates a rich text version of your severity category that is colored using your configured color code. The `severity_rt` attribute only styles the color of the text run so that you can apply a paragraph style to it directly in your Word template. Use it with the `r` tag (for a run) like so: That template renders as: ### Ghostwriter Tags Several tags used for Word documents are not built into Jinja2. These tags are added after you open an expression or statement (before the space). Example: `{{p findings_subdoc }}` | **Tag** | **Description** | | ------- | --------------- | | `r` | Text run | | `p` | New paragraph | | `tr` | Table row | | `tc` | Table column | The table tags may appear complicated at first. You can create a table with a row for each point of contact using the provided statements, expressions, and tags like this: Per `python-docx-template`, do not use `{%p`, `{%tr`, `{%tc` or `{%r` twice in the same paragraph, row, column, or run. Bad: ``` {%p if display_paragraph %}Here is my paragraph {%p endif %} ``` Good: ``` {%p if display_paragraph %} Here is my paragraph {%p endif %} ``` ### Ghostwriter Statements There are several statements for Word documents that are not built into Jinja2: | **Statement** | **Description** | | --------------------------- | ----------------------------------------------------------------- | | `{% cellbg color_var %}` | Color a table cell where `color_var` is a hex value without the # | | `{% colspan some_number %}` | Span a table cell over a `some_number` of columns | ### Ghostwriter Filters Ghostwriter offers some custom filters you can use to modify report values quickly: The filter collection is under development and will continue to grow. | **Filter** | **Usage** | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `filter_severity(list)` | Accepts the `findings` variable and filters it with a list of severities. **Example:** This statement loops over only findings rated as *High* or *Medium* severity: `{% for x in findings \| filter_severity(["High", "Medium"]) %}` | | `strip_html(string)` | Accepts HTML strings and strips all tags. | | `compromised(targets)` | Accepts `targets` value and filters it to only include hosts marked as compromised. | | `filter_type(list)` | Accepts the `findings` variable and filters it with a list of categories. **Example:** This statement loops over only findings with the type *Network*: `{% for x in findings \| filter_type(["Network"]) %}` | | `add_days(date, days)` | Provide a date and a number of days (integer) to add or subtract. Use negative numbers for subtraction. **Examples:** `"February 1, 2025" \| add_days(-10)` and `{{ project.start_date }} \| add_days(-10)` return "2025 Jan 20" (where `project.start_date`: 2025 Feb 01) | | `format_datetime(date, format_str)` | Provide a date and a format string. Use [Django date format strings](https://docs.djangoproject.com/en/3.2/ref/templates/builtins/#std-templatefilter-date). **Examples:** `"February 1, 2025" \| format_datetime("N j, Y")` and `{{ project.start_date }} \| format_datetime("N j, Y")` return "Feb. 1, 2025" (where `project.start_date`: 2025 Feb 01) | | `to_datetime(date, format_str)` | Provide a date and a format string. Use [Python datetime format strings](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes). **Examples:** `"Feb 20, 2025" \| to_datetime("%b %d, %Y")` and `{{ project.start_date }} \| to_datetime("%b %d, %Y")` return datetime.datetime(2025, 2, 1, 0, 0) (where `project.start_date`: 2025 Feb 01) | | `business_days(start_date, end_date)` | Provide two dates and calculate the amount of business days between them. **Example:** `"December 1, 2025" \| business_days("December 12, 2025")` return 10 | | `get_item(list, index)` | Provide a list and an index to retrieve the list item at that index. **Example:** `["ghostwriter", "report", "ghost"] \| get_item(0)` returns `ghostwriter` | | `filter_tags(list, allowlist)` | Accepts a list of objects (e.g., `findings`) and filters it with a list of tags. **Example:** This statement loops over only findings tagged with `xss`: `{% for x in findings \| filter_tags(["xss"]) %}` | | `regex_search(text, regex)` | Perform a search with a regular expression and get the first match. | | `replace_blanks(list, placeholder)` | Replace null dictionary keys with `""` (default) or the specified placeholder value. **Example:** Attempting to use Jinja2's `sort` filter with a list of dictionaries with null values will cause an error. This statement loops over all entries in an activity log while also replacing blank values and then sorting: `{% for entry in log \| replace_blanks \| sort(attribute="tool") %}` | ### Subdocuments Subdocuments are like other variables, except they are pre-rendered Word documents. Inserting a subdocument is like copying and pasting content from one document into another. A subdocument can be a small paragraph or a much larger section. Ghostwriter uses subdocuments to translate your WYSIWYG content (e.g., findings) to Office Open XML. Subdocuments are referenced as `{{p VARIABLE }}`. That variable is automatically replaced with the contents of the subdocument. ## Debugging a Template Ghostwriter uses the `jinja2.ext.debug` extension to make it easier for you to debug a template. Place a `{% debug %}` tag somewhere in your template. The next time you generate a report with that template, Ghostwriter will replace the tag with the template's available context (the report and project data) and filters. Also, see [Troubleshooting Word Templates](/features/reporting/report-templates/troubleshooting-word-templates) for a more in-depth explanation of how to troubleshoot a template that gives you problems. # Overview Source: https://www.ghostwriter.wiki/features/reporting/report-types Introduction to the built-in report types ## Introducing the Report Types Ghostwriter supports a variety of report types: * Raw JSON * Microsoft Office 2019 / 365 * Word (docx) * Excel (xlsx) * PowerPoint (pptx) ### Selecting a Report Type While viewing a report, scroll down to the **Generate Reports** section to configure your report and select the type of report you want. ### JSON The JSON output is the foundation of every other report type. It is surfaced as a report type to enable users to take it and create custom reports and visualizations. Example Report JSON ```json ExampleJSONReport.json theme={"system"} { "client": { "id": 1, "full_name": "Kabletown", "short_name": "KT", "codename": "SCHEMING RANGER", "poc": { "1": { "id": 1, "name": "John Francis Donaghy", "job_title": "Vice President of East Coast Television and Microwave Oven Programming", "email": "jack@nbc.com", "phone": "555-123-4556", "note": "Goes by \"Jack.\"" }, } }, "project": { "id": 1, "name": "Kabletown Penetration Test (GREATER THUNDER)", "start_date": "2019-07-29", "end_date": "2019-08-09", "codename": "GREATER THUNDER", "project_type": "Penetration Test", "note": "" }, "findings": { "SMBv1 Remote Code Execution (CVE-2017-0143)": { "id": 1, "title": "SMBv1 Remote Code Execution (CVE-2017-0143)", "severity": "High", "affected_entities": "...", "description": "...", "impact": "...", "recommendation": "...", "replication_steps": "...", "host_detection_techniques": "...", "network_detection_techniques": "...", "references": "...", "evidence": { "Enigma": { "id": 5, "friendly_name": "Enigma", "uploaded_by": "admin", "upload_date": "2019-08-04", "description": "Captured while he was discovering a new 0-day, probably.", "caption": "Matt Nelson, OSCP in the zone", "url": "/media/evidence/1/matt_nelson.png", "file_path": "evidence/1/matt_nelson.png" } } }, }, "infrastructure": { "domains": { "1": { "id": 1, "name": "getghostwriter.io", "activity": "Command and Control", "operator": "admin", "start_date": "2019-07-15", "end_date": "2019-08-09", "note": "Used with Covenant C2." } }, "servers": { "static": { "1": { "id": 1, "ip_address": "159.89.234.80", "activity": "Command and Control", "role": "Team Server / C2 Server", "operator": "benny", "start_date": "2019-07-29", "end_date": "2019-08-09", "note": "Used for Covenant C2." } }, "cloud": { "1": { "id": 1, "ip_address": "255.255.255.123", "activity": "Phishing", "role": "SMTP", "operator": "benny", "note": "" }, }, }, "domains_and_servers": { "1": { "domain": "code.getghostwriter.io", "servers": "255.255.255.123", "cdn_endpoint": "ghost-cdn.azureedge.net" }, } }, "team": { "1": { "id": 1, "name": "Benny Ghostwriter", "project_role": "Assessment Lead", "email": "benny@ghostwriter.wiki", "start_date": "2019-07-29", "end_date": "2019-08-09", "note": "..." }, } } ``` ### Office Documents Microsoft Office is ubiquitous and a staple of many reporting workflows. To that end, Ghostwriter's reporting engine supports the construction and export of all three major Office file types: Word documents, Excel spreadsheets, and PowerPoint slide decks. You can customize each of these, so the exported files fit your specific needs. #### Word Documents For Word, Ghostwriter renders a *docx* file using the JSON data and a report template. Review the Word section for more information on customization options and how to get started generating Word documents. #### PowerPoint Slide Decks For PowerPoint, Ghostwriter renders a *pptx* file using the JSON data and a slide deck master. Review the PowerPoint section for more information on customization options and how to get started generating slide decks. #### Excel Spreadsheets For Excel, Ghostwriter renders an *xlsx* file using the JSON data. Review the Excel section for more information on customization options. # Excel Spreadsheet Customization Source: https://www.ghostwriter.wiki/features/reporting/report-types/excel-spreadsheet-customization Customizing Excel spreadsheet generation ## Getting Started with Excel No configuration is needed to generate a spreadsheet of report details. Open you report, scroll down to the **Generate Reports** section. and click the Excel icon to generate a spreadsheet. Depending on the size of your report, rendering can take a few seconds. Once the report is done, your browser will download a new Excel document. The default filename will be: `YYYYMMDD_HHMMSS_CLIENT-NAME_ASESSMENT-TYPE.xlsx` ### Spreadsheet Contents The spreadsheet's current version contains all the findings attached to your report. The first worksheet will contain a formatted table of your findings sorted by severity. The severity column will be color-coded with your configured severity colors. It is impossible to insert evidence files into the spreadsheet cleanly, so it has an **Evidence** column. Ghostwriter replaces any references to evidence files with an entry in the **Evidence** column, so readers know there is additional evidence available for review. # PowerPoint Deck Customization Source: https://www.ghostwriter.wiki/features/reporting/report-types/powerpoint-deck-customization Customizing PowerPoint slide deck generation ## Getting Started with PowerPoint Ghostwriter uses template documents and the Jinja2 template language ([https://jinja.palletsprojects.com/en/stable/](https://jinja.palletsprojects.com/en/stable/)) to give you as much control over document generation as possible. Learn more about managing report templates here: [Report Templates](/features/reporting/report-templates) You will need to upload at least one PowerPoint slide deck to use as a template. Once you have a template you can pick from, open your report and select the template from the dropdown menu under the **Generate Reports** section. You should see a notification when the template selection is saved. You can then click the PowerPoint icon to generate a report. Depending on the size of your report and template, rendering can take a few seconds. Once the report is done, your browser will download a new PowerPoint document. The default filename will be: `YYYYMMDD_HHMMSS_CLIENT-NAME_ASESSMENT-TYPE.pptx` ### PowerPoint Templates There are fewer customization options for PowerPoint than Word. Your template slide deck controls how the generated slide deck looks, but the content will always be the same. The PowerPoint template should be empty – i.e., should contain zero slides. Edit the master slides to control colors, layout, slide numbers, and other details. To open your slide master view in PowerPoint: *View* » *Slide Master* PowerPoint remembers if you closed the deck with the Slide Master view open or not and will re-open where you left off. To avoid every presentation opening on the view, close it before saving and uploading your template. Your templates should include at least three slide layouts in your *Slide Master* view. Your first layout must be your title slide layout, the second layout must be your content layout, and your last layout must be your final/conclusion slide layout. Ghostwriter uses these three layouts, but you are welcome to include additional layouts between the content layout and the final slide layout. Just ensure the layout for your conclusion slide is last. Ghostwriter will attempt to intelligently use the placeholders you include in your layouts. The slide's title (type 1 placeholder) will go into the slide's title placeholder. Likewise, when Ghostwriter adds a subtitle to the title slide, it will try to use a subtitle placeholder (type 4 placeholder), if present. Finally, you want a content placeholder (type 7 placeholder) for the slide's main body. Adding text boxes and other objects does not create placeholders. These are "shapes" that will always be present on slides created with the layout. If the expected placeholders are not present, Ghostwriter will try to fallback to using detected shapes. If there are no shapes, the reporting engine will create new shapes. Ghostwriter tries to make new shapes in the same position where title and content are often placed, but it's a best guess on our part. Make use of placeholders for the best output! Title and subtitle are special placeholders. Each slide can contain one of each. You can verify you have these placeholders in a couple of different ways. You can see if you have a title placeholder by looking at your layout and verifying the *Title* checkbox is checked under the *Slide Master* section of the ribbon menu. It's a good idea to toggle it off and on to verify the placeholder you expect to be the title is really the title. If someone manually moved the title to repurpose it (e.g., for content or as a footer placeholder), that placeholder is still the title for that layout. Subtitles are trickier because PowerPoint actually offers no way to create these from the *Insert Placeholder* menu. There is also no checkbox to create one like there is for *Title* or *Footers*. Many default PowerPoint layouts include them, which is one way to get them. You can also copy and paste a subtitle placeholder from another slide. Finally, you can use a macro to insert a subtitle placeholder. These two macros can be helpful for identifying the type of placeholder you have selected and inserting a subtitle placeholder on your title slide (for use or copy/pasting): ```vb theme={"system"} Sub InsertSubtitlePlaceholder() Dim oSlideMaster As Master Dim oLayout As CustomLayout Dim oSubtitleShape As Shape ' Reference the first slide master Set oSlideMaster = ActivePresentation.SlideMaster ' Reference the first layout (or specific layout) Set oLayout = oSlideMaster.CustomLayouts(1) ' Add a subtitle placeholder Set oSubtitleShape = oLayout.Shapes.AddPlaceholder(Type:=ppPlaceholderSubtitle, _ Left:=100, Top:=300, Width:=500, Height:=100) oSubtitleShape.TextFrame.TextRange.Text = "Subtitle Placeholder" End Sub Sub ShowPlaceholderType() With ActiveWindow.Selection.ShapeRange If .Type = msoPlaceholder Then Select Case .PlaceholderFormat.Type Case ppPlaceholderTitle MsgBox "Title Placeholder" Case ppPlaceholderCenterTitle MsgBox "Centered Title Placeholder" Case ppPlaceholderSubtitle MsgBox "Subtitle Placeholder" Case ppPlaceholderObject MsgBox "Content Placeholder" End Select End If End With End Sub ``` For further reference: [PowerPoint PpPlaceholderType Enumeration](https://learn.microsoft.com/en-us/office/vba/api/powerpoint.ppplaceholdertype) ### PowerPoint Generation Ghostwriter will add slides to your chosen template slide deck. The new slides include placeholders and dynamic project information. Each slide is set to fit text to the size of the text field. This auto-resizing happens in PowerPoint's rendering engine, so a slide's text might extend beyond the slide's lower boundary when you open your new presentation. PowerPoint should activate auto-resizing when you save the presentation or edit any text. | Slide Type | Description | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Title Slide | Includes your configured company name, selected project type, and client name | | Agenda Slide | Placeholder for you to enter a meeting agenda | | Introduction Slide | Placeholder for any presenter introductions | | Methodology Slide | Placeholder for reviewing testing methodology | | Attack Path Overview Slide | Placeholder for where you might discuss assessment narratives | | Findings Overview Slide | Includes a two-column table showing all findings (full title and severity) | | Findings Slides | One slide per finding that includes:
• Finding title as the slide title
• All image evidence files as inserted images
• All text evidence files as new text blocks (styled with a fixed-width font)
• Finding description as main slide content
• All other finding information in the slide's notes field | | Observations Slide | Placeholder for any additional observations | | Recommendations Slide | Placeholder for any recommendations | | Conclusion Slide | Placeholder for closing statements or next steps | | Final Slide | Closing title slide that includes your configured company name, social media account, and email address | # Word Document Customization Source: https://www.ghostwriter.wiki/features/reporting/report-types/word-document-customization Customizing Word document generation ## Getting Started with Word Ghostwriter uses template documents and the Jinja2 template language ([https://jinja.palletsprojects.com/en/2.11.x/](https://jinja.palletsprojects.com/en/2.11.x/)) to give you as much control over document generation as possible. Learn more about managing report templates here: [Report Templates](/features/reporting/report-templates) You will need to upload at least one Word document to use as a template. Once you have a template uploaded, open your report and select the template from the dropdown menu under the **Generate Reports** section. You should see a notification when the template selection is saved. You can then click the Word icon to generate a report. Depending on the size of your report and template, rendering can take a few seconds. Once the report is done, your browser will download a new Word document. The default filename will be: `YYYYMMDD_HHMMSS_CLIENT-NAME_ASESSMENT-TYPE.docx` ### Word Templates Your templates can be simple documents or complete reports. You can convert those to Ghostwriter templates if you currently manage one or more report templates for different projects. One of the simplest examples of how Ghostwriter can save a team time and effort is replacements. You can create complex dynamic Word templates for Ghostwriter, but the most basic Jinja2 expression is a simple variable, like this one: `{{ report_date }}` Ghostwriter will replace every instance of `{{ report_date }}` in your template with the current date (e.g., October 31, 2020). The replacement does not affect any formatting or styling, so this variable becomes the current date and keeps the original font style, color, and placement. Everything seen in the report [JSON](/features/reporting/report-types#json) is accessible as a variable within your template. You can also use additional variables, filters, and expressions to refine or customize the report content. You decide what to include in your templates. Learn more about the available filters, expressions, and variables in this section: [Word Template Variables & Filters](/features/reporting/report-templates/word-template-variables) ### Inserting Evidence Ghostwriter will insert any evidence files you have added to your report. The report will have the evidence file's contents followed by your caption. The default behavior is slightly different for images and text. #### Inline Image Evidence The report engine will insert images with the following default characteristics: * Center aligned * 6.5" wide (full page width) * You can adjust the width to be used in the configuration for your Word report templates * Image border * You can turn borders on or off and adjust the weight and color of image borders in the global report configuration * Figure caption using your label and prefix character * You can adjust the label and prefix character (e.g., *Figure —*, *Evidence:*) in the global report configuration * You can also adjust placement (above or below) in the global report configuration #### Inline Text Evidence The report engine will insert text evidence with the following default characteristics: * Applies **Code Block** style * Left aligned text (per above style) * Figure caption using your label and prefix character * You can adjust the label and prefix character (e.g., *Figure —*, *Evidence:*) in the global report configuration * You can also adjust placement (above or below) in the global report configuration You can edit the weight and color of image borders and the character that follows *Figure* in the global report configuration. [Configuring Global Report Options](/configuring-global-settings/configuring-global-report-options) # Templating and Rich Text Fields Source: https://www.ghostwriter.wiki/features/reporting/templating-and-rich-text-fields DOCX templates uploaded to Ghostwriter and rich text fields in Ghostwriter are both processed with Jinja 2, which will replace text from the document with data from the report. For example, you can write `{{ title }}` in a finding description and it will be replaced by the report title when generating a report, without having to edit it manually. Jinja has extensive documentation [on their web site](https://jinja.palletsprojects.com/en/3.1.x/templates/), which is a good starting point. For Word DOCX files, Ghostwriter uses the [python-docx-template library](https://docxtpl.readthedocs.io/en/latest/#jinja2-like-syntax) which adds a few extensions. ## Available Variables All templates have access to the report data. To view what that includes, select the "Generate exportable JSON" option from the Generate Report page. For example, the report title is available as `{{ title }}` and the project title through `{{ project.title }}`. In addition, the rich text fields on a finding have an additional `finding` variable, which is a copy of the object from the `findings` array in the report. ## Tag Prefixes When inserting rich text fields, or when wanting to add list items or table rows in a loop, a prefix on the Jinja tag must be used. These tag prefixes work by **replacing** the prefix's corresponding element, to ensure that the inserted elements are properly nested. Because the element is replaced, **you should not place anything else along side a prefixed tag**. ### Inserting Rich Text To insert a rich text field inside of a paragraph in another rich text field or a DOCX template, use `{{p rich_text_variable}}`. This will replace the tag's paragraph with the paragraphs in the rich text field. For example: > Report Additional Info: > > \{\{p extra\_fields.additional\_info}} ### Looping To generate multiple paragraphs in a loop: > \{%p for finding in findings %} > > \{\{finding.name}} > > \{\{p finding.description}} > > \{%p endfor %} By specifying `p` on the for and endfor statements, the paragraphs they are in will be deleted, so that they don't cause extra newlines in the document. To generate a list in a DOCX template, use the `p` prefix: > * \{%p for tag in tags %} > > * \{\{tag}} > > * \{%p endfor %} To generate a list in a rich text field, use the `li` prefix: > * \{%li for tag in tags %} > > * \{\{tag}} > > * \{%li endfor %} To generate a table in a DOCX template or rich text field, use the `tr` prefix: | **Finding** | **Severity** | | --------------------------------- | ------------------------- | | `{%tr for finding in findings %}` | | | `{{finding.name}}` | `{{finding.severity_rt}}` | | `{%tr endfor %}` | | Just like with the `p` prefix, `li` will replace the list element and `tr` will replace the entire table row. ## Functions and Filters Ghostwriter provides the following extensions in rich text fields: * `{{.evidence name}}` or `{{mk_evidence("name")}}`: Inserts the evidence with the specified friendly name. Rich text fields on findings will search the finding's evidence; others search through the report evidence. Also creates a Word bookmark. * `{{.ref name}}` or `{{mk_ref("name")}}`: Makes a reference to a Word bookmark, usually generated by `{{.evidence}}` or `{{.caption}}` above. The text will go to the bookmark when ctrl+clicked. * `{% for finding in findings|filter_severity(["Critical", "High"]) %}`: Filters findings by severity to those in the list. * `{% for finding in findings|filter_type(["Network", "Web"]) %}`: Filters findings by type to those in the list. * `{{text|strip_html}}`: Removes HTML tags from the text while adding newlines based on `

` and `
` tags. * `{% for target in targets|compromised %}`: Filters targets by those marked as compromised. * `{{ start_date|add_days(1) }}`: Adds an number of business days to the date. * `{{ start_date|format_datetime("Ymd") }}`: Formats a date using [Django date format strings](https://docs.djangoproject.com/en/4.2/ref/templates/builtins/#std-templatefilter-date). * `{{ findings|get_item(0) }}`: Gets an item from a list or dictionary. Unlike `foo.bar` in Jinja, this forcibly uses the Python indexing operator, so it will not conflict with attributes. * `{{ title|regex_search("a-z+") }}`: Performs a regex search on a string, returning the match or None. * `{% for finding in findings|filter_tags(["xss", "T1651"]) %}`: Filters objects to include only ones that have any of the tags in the list. ## Escaping If you need to escape Jinja syntax so that you can emit text like "\{\{" in your document, you have two options: First, you can use `{{ "{{" }}`, `{{ "{%" }}`, etc., to emit a string literal. This is convenient for one-off replacements. For larger blocks, you can use Jinja's `{% raw %} ... {% endraw %}` blocks. Content in the block will not be parsed as Jinja tags and will be emitted as-is. # Overview Source: https://www.ghostwriter.wiki/getting-help/faq Common problems and questions ### Ghostwriter CLI Reports an Issue with PostgreSQL You may encounter an issue with PostgreSQL while upgrading an existing installation. Ghostwriter v2.x.x and lower used environment variables stored in files under *.envs/.production/.postgres*. Beginning with v3.0.0, Ghostwriter no longer uses the *.envs/* files and you must transfer your PostgreSQL username and password to the new configuration file. By default, Ghostwriter CLI generates a random password for a default `postgres` user. Update the configuration with your Postgres credentials by running these commands: ```log theme={"system"} ./ghostwriter-cli config set POSTGRES_USER ./ghostwriter-cli config set POSTGRES_PASSWORD ``` If that does not work or you need to change the credentials, you may do so with the `psql` tool. First, start the containers even if initialization fails due to the bad password. ```log theme={"system"} docker exec -it ghostwriter_postgres_1 bash psql -U postgres ``` Use the `\password` command to set a new password for the `postgres` user: ```log theme={"system"} postgres=# \password postgres Enter new password: Enter it again: postgres=# \q ``` If `postgres` is not your username, change the command to use your chosen username. If you are not sure what the username is, run the `\du` command: ```log theme={"system"} postgres=# \du List of roles Role name | Attributes | Member of -----------+------------------------------------------------------------+----------- postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {} ``` That sets the new password and `\q` quits the `psql` console. Set your new password in Ghostwriter's config, and then bring the containers down and back up. ```log theme={"system"} ./ghostwriter-cli containers down ./ghostwriter-cli config set POSTGRES_PASSWORD ./ghostwriter-cli containers up ``` If the passwords still do not match, you may need to try again. Copy/pasting the password may not work. If you try to paste the new password, you might not be setting the password you expect. It is best to type the password manually. ## Stuck Waiting for Django to Start / 502 Bad gateway There will be times something goes wrong during the Docker build. One of the most common issues is filesystem permissions are not being set correctly. Ghostwriter runs under the `django` user, not `root`. The `django` user should own all files under the */app* directory. If you are stuck waiting for Django to start, run this command and check file permission errors: `./ghostwriter-cli logs django` If you see file permission errors, check the user and group permissions for some of the affected files like so: ```log theme={"system"} docker-compose -f production.yml run django ls -la /app docker-compose -f production.yml run django ls -la /app/staticfiles ``` A properly configured listing will look like this: ```log theme={"system"} $ docker-compose -f production.yml run django ls -la /app Creating ghostwriter_django_run ... done PostgreSQL is available total 64 drwxr-xr-x 1 django root 4096 Feb 18 00:17 . drwxr-xr-x 1 root root 4096 Feb 18 00:42 .. -rw-r--r-- 1 django root 598 Oct 29 2021 .coveragerc -rw-r--r-- 1 django root 242 Dec 1 2020 .pylintrc -rw-r--r-- 1 django root 24 Feb 18 00:15 VERSION « snip » $ docker-compose -f production.yml run django ls -la /app/staticfiles Creating ghostwriter_django_run ... done PostgreSQL is available total 76 drwxr-xr-x 15 django django 4096 Feb 23 2022 . drwxr-xr-x 1 django root 4096 Feb 18 00:17 .. drwxr-xr-x 6 django django 4096 Oct 1 2019 admin drwxr-xr-x 4 django django 4096 Feb 18 00:34 css « snip » ``` If you see anything else, you may need to re-run the `chown` command that Docker is supposed to run during the build. It must be run as `root`. Use Docker Compose's `run` command like the above examples but add `-u root`: ```log theme={"system"} $ docker-compose -f production.yml run -u root django chown -R django:django /app ``` Once that runs, verify the `ls -la` command shows the proper permissions, and then try restarting the Ghostwriter services: ```log theme={"system"} ./ghostwriter-cli containers down ./ghostwriter-cli containers up ``` # Reporting a Problem Source: https://www.ghostwriter.wiki/getting-help/getting-help-with-a-problem How to report a problem or get support ## Getting Help Oh no! Something went wrong, and you need help or want to report a problem. The best place to start is right here, the wiki. Many common issues are caused by missing a step in the Installation section. Please make sure you have followed each step and seeded the database. If everything looks good and you still have problems, pick one of these options. ### Submit an Issue or Bug Document your problem in a GitHub Issue. Include the following information: * What you tried to do * What you expected to happen * What happened * Relevant stack traces, screenshots, or other information ### Discuss a Question or Problem Feel free to open a GitHub Issue to discuss your question, but you may get a quicker response via Slack. # Reporting a Security Issue Source: https://www.ghostwriter.wiki/getting-help/reporting-a-security-issue How to report vulnerabilities ## Reporting Vulnerabilities If you identify a security vulnerability, please open a GitHub Issue. Include the following information: * A concise description of the vulnerability and its impact * Step-by-step instructions to reproduce or observe the issue * Relevant stack traces, screenshots, or other information The development team will respond as soon as possible. Depending on the impact and work required to resolve the issue, expect a patch within 7-14 days. Fixed vulnerabilities will be closed and documented in release notes. If a vulnerability is not fixed for any reason, the issue will be closed, and why the vulnerability will not or can not be fixed will be documented. If you need to disclose a vulnerability that may be sensitive, you may disclose the issue privately via email. Send such reports to *[info@getghostwriter.io](mailto:info@getghostwriter.io)*. # Managing the Server with Ghostwriter CLI Source: https://www.ghostwriter.wiki/getting-started/managing-the-server-with-ghostwriter-cli How to use Ghostwriter CLI to monitor and manage your Ghostwriter installation Ghostwriter CLI reads and writes to the `.env` file and tries to establish an outbound network connection when you run the `update` and `version` commands. The CLI only reads and writes to Ghostwriter's `.env` file. For network connectivity, it only connects to GitHub's REST API to pull the version information for Ghostwriter and Ghostwriter CLI's latest tagged releases. However, these actions taken by a downloaded binary will cause some anti-malware and EDR solutions to flag Ghostwriter CLI as malware. If you encounter an issue with such a solution blocking Ghostwriter CLI from executing, please add the binary to your allowlist or exclusions list. We are working to try to get Ghostwriter CLI to be trusted, as it once was, but these issues come and go. If you have concerns about this behavior, you can build Ghostwriter CLI from source. See the [GitHub repository](https://github.com/GhostManager/Ghostwriter_CLI). Further, you can inspect the source code to verify its behavior and check the build hashes against the [releases page](https://github.com/GhostManager/Ghostwriter_CLI/releases). ## Using Ghostwriter CLI to Manage the Server Ghostwriter CLI can handle just about anything you might need or want to do with your Ghostwriter installation. Running the tool with the `help` command–or no command–will print the latest usage information. The following sections explain some of the core functionality. By default, all commands target a production environment. Provide the `--dev` if you're interacting with a development environment. Every command can accept the `--dev` flag. ### Managing the Server Configuration The `config` command is your Swiss Army knife when managing your server configuration. If you don't provide any subcommands or arguments, the command prints your current configuration values (the contents of your server's DotEnv file). You can pull and set configuration values with `config`. Use `config get` to fetch a specific value or set of values. Use `config set` to change a value. There are also several subcommands to help you manage hostnames and origins you trust: `allowhost`, `disallowhost`, `trustorigin`, and `distrustorigin`. Use these subcommands to adjust those values, as the Quick Start guide describes. ### Managing Docker Containers Ghostwriter CLI's `containers` command contains the following subcommands: * `build` : Rebuild the containers (not the data volumes) * `up` : Bring up Ghostwriter containers * `down` : Bring down Ghostwriter containers * `start`: Start all stopped services and restart any running services * `stop`: Stop all running services * `restart` : Stop and restart all services If you need to check which containers are running, issue the `running` command. This command lists all running containers related to Ghostwriter. The output will look something like this (edited for easier display): ```log Running Containers theme={"system"} $ ./ghostwriter-cli running [+] Collecting list of running Ghostwriter containers... [+] Found 4 running Ghostwriter containers Name Container ID Image Status Ports –––––––––––– –––––––––––– –––––––––––– –––––––––––– –––––––––––– ghostwriter_graphql d90e.... ghostwriter_local_graphql Up 43 hours (healthy) 0.0.0.0:9691:9691 » 9691/tcp, 0.0.0.0:8080:8080 » 8080/tcp ghostwriter_queue 0559.... ghostwriter_local_queue Up 43 hours (healthy) ghostwriter_django bf9b.... ghostwriter_local_django Up 43 hours (healthy) 0.0.0.0:8000:8000 » 8000/tcp ghostwriter_postgres 9992.... ghostwriter_production_postgres Up 43 hours (healthy) 0.0.0.0:5432:5432 » 5432/tcp ``` The `Status` column shows the uptime and health of the service. If you see an `unhealthy` status that the service failed a health check and may not work properly. Learn more about health checks here: [Health Monitoring](/features/health-monitoring) You can use the `logs` command to view a particular container's recent log events. The command requires the name of a running container. Valid container names are: * all * django * graphql * nginx * postgres * redis * collab With `all`, logs from all running containers will be returned. By default, `logs` will return up to 500 lines. You can use the `--lines` flag to adjust how many lines you want to retrieve. ```log Viewing Django Logs theme={"system"} $ ./ghostwriter-cli logs django PostgreSQL is available No changes detected Operations to perform: Apply all migrations: account, admin, api, auth, commandcenter, contenttypes, django_q, home, oplog, reporting, rest_framework_api_key, rolodex, sessions, shepherd, sites, socialaccount, users Running migrations: No migrations to apply. INFO: Will watch for changes in these directories: ['/app'] INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) INFO: Started reloader process [18] using watchgod INFO: Started server process [20] INFO 2022-06-06 21:47:37,673 server 20 140231754545992 Started server process [20] INFO: Waiting for application startup. INFO 2022-06-06 21:47:37,673 on 20 140231754545992 Waiting for application startup. INFO: ASGI 'lifespan' protocol appears unsupported. INFO 2022-06-06 21:47:37,674 on 20 140231754545992 ASGI 'lifespan' protocol appears unsupported. INFO: Application startup complete. INFO 2022-06-06 21:47:37,674 on 20 140231754545992 Application startup complete. ``` # Creating and Managing Backups Source: https://www.ghostwriter.wiki/getting-started/managing-the-server-with-ghostwriter-cli/creating-and-managing-backups Using Ghostwriter CLI to create and manage backups ## Creating and Managing Backups Ghostwriter CLI (>=v0.2.12) includes `backup` and `restore` commands to help you create and manage backups of your PostgreSQL database. The `backup` command uses PostgreSQL's `pg_dump` to dump the contents of the database. The resulting SQL file is gunzipped and stored in the `production_postgres_data_backups` Docker volume. Each file is named with the current timestamp (e.g., *backup\_2023\_05\_23T15\_54\_19.sql.gz*). Use the `--list` flag to list all available backup files stored in the volume. The `restore` command recreates the database using the specified backup file. # Managing Logon Sessions Source: https://www.ghostwriter.wiki/getting-started/managing-the-server-with-ghostwriter-cli/managing-logon-sessions Managing cookie age and logon session expirations You can adjust session management with three configuration values: * *DJANGO\_SESSION\_COOKIE\_AGE* * Sets the number of seconds a session cookie will last before expiring (default: *32400* seconds or nine hours) * *DJANGO\_SESSION\_SAVE\_EVERY\_REQUEST* * Sets whether the session cookie will refresh on every request (default: *true*) * *DJANGO\_SESSION\_EXPIRE\_AT\_BROWSER\_CLOSE* * Sets whether the session cookie will expire when the browser is closed (default: *true*) These defaults are a good starting point. Still, you should consider how your team uses Ghostwriter and adjust accordingly. We chose nine hours for the expiration, so a session can last an entire workday — just in case someone is in the middle of something and has to walk away for an extended period. You may want to reduce this value to one or two hours. With *DJANGO\_SESSION\_SAVE\_EVERY\_REQUEST* set to *true*, the server will update the session with each request. Updates reset the expiration, so a short expiry period won’t log out anyone actively using Ghostwriter but will allow inactive sessions to expire. If set to *true*, the last option will expire sessions after the browser quits. However, whether the session ends when you close the browser window depends on the browser. Some browsers, like Chrome, will keep sessions active, so you may need to quit or exit the browser to end the session versus just closing the browser window. You can manage these values via the Ghostwriter command-line interface (CLI) tool. # Populating a Demo Database Source: https://www.ghostwriter.wiki/getting-started/populating-a-demo-database How to rebuild a local Ghostwriter database with realistic demo data Ghostwriter includes a management command that rebuilds a development or demo database with realistic sample data. Use it when you need a fresh environment for demos, training, screenshots, testing workflows, or validating report exports. The default command **wipes the database and uploaded media** before loading demo data. Do not run it against a production database or any environment with data or files you need to keep. The media directory itself is preserved so the command works when `MEDIA_ROOT` is a Docker volume or bind mount; only its contents are removed. The command validates the media path and bundled templates before flushing the database. ## What the Command Creates The demo data command performs the same baseline setup as a fresh install and then adds realistic Ghostwriter content: * Clears uploaded media and reloads all Ghostwriter initial fixture data, including the optional default report templates * Creates or updates the admin user using the username, email, and password from the DotEnv configuration * Assigns the admin user to every active project and checks out a domain and server to the admin so the dashboard and **My Active Assets** page are populated immediately after login * Creates demo users with names, emails, passwords, and user profiles * Creates clients, contacts, projects, four-person project teams with one lead, one oversight, and two operators, scopes, targets, objectives, deconflictions, white cards, operation logs, findings, observations, reports, and evidence * Creates believable examples of every extra field type, including project fields such as **Entity Tested** and **Include CVSS**, plus report fields for **Reviewed**, **Attack Path Narrative**, and **Executive Summary** * Adds sanitized Nmap, Burp Proxy, and Rubeus text evidence from `DOCS/example-data` * Applies demo company information and report configuration * Enables the global BloodHound API configuration with placeholder values and loads cached demo BloodHound results Project dates are generated relative to the day you run the command. The full dataset includes completed work in the past, projects overlapping the current date, and upcoming work in the future. ## Run the Command For a local development checkout, run the command in the Django container: ```bash theme={"system"} docker compose -f local.yml run --rm django python manage.py generate_test_data ``` After it finishes, log in as the configured admin user. If you used the Ghostwriter CLI installer, retrieve the admin password from the DotEnv configuration: ```bash Linux theme={"system"} ./ghostwriter-cli config get ADMIN_PASSWORD ``` ```powershell Windows PowerShell theme={"system"} .\ghostwriter-cli config get ADMIN_PASSWORD ``` ```bash Mac theme={"system"} ./ghostwriter-cli config get ADMIN_PASSWORD ``` ## Create a Smaller Dataset Use `--quick` when you want enough data for a complete walkthrough without filling every demo client and project: ```bash theme={"system"} docker compose -f local.yml run --rm django python manage.py generate_test_data --quick ``` By default, the command creates all three available demo clients. The `--quick` option creates one client. The `--clients` flag is capped at the three built-in demo clients, so its useful middle setting is `--clients 2`: ```bash theme={"system"} docker compose -f local.yml run --rm django python manage.py generate_test_data --clients 2 ``` Use `--projects-per-client` when you want to limit how many projects are created for each selected client: ```bash theme={"system"} docker compose -f local.yml run --rm django python manage.py generate_test_data --clients 2 --projects-per-client 1 ``` ## Append Instead of Wiping If you want to keep existing data and only add or update demo rows, use `--append`: ```bash theme={"system"} docker compose -f local.yml run --rm django python manage.py generate_test_data --append ``` Append mode is idempotent for the seeded demo rows. It stops with an error rather than overwrite an unrelated client, project, finding, domain, or server that uses the same identifying value as a demo record. To remove previous demo rows before appending fresh demo data, add `--reset`: ```bash theme={"system"} docker compose -f local.yml run --rm django python manage.py generate_test_data --append --reset ``` `--reset` only removes rows marked as demo seed data. It does not wipe unrelated rows unless you omit `--append`, in which case the default database rebuild behavior applies. Because a normal run already performs the full rebuild, `--reset` is only necessary together with `--append`. The standard rebuild is deterministic apart from dates being relative to the day it runs. Re-running it produces the same clients, projects, assignments, findings, and related demo content on a clean database and media directory. # Quickstart Source: https://www.ghostwriter.wiki/getting-started/quickstart Get started with Ghostwriter quickly and easily with Docker and Ghostwriter CLI ## Prerequisites Ghostwriter deploys in a traditional multi-tier container architecture consisting of database, application, and UI layers. To complete installation, ensure your system meets the following requirements: | Minimum specifications | For larger teams (>5 users) | | ---------------------- | --------------------------- | | 2GB of RAM | 16GB of RAM | | 2 processor cores | 4 processor cores | | 10GB hard disk space | 60GB hard disk space | Ghostwriter includes features like full collaborative editing that require more resources as the number of simultaneous active users increases. For a smooth experience, we recommend the higher system requirements for production use. That is equivalent to a `t3.xlarge` instance in AWS. For ease and convenience, we recommend installing [Docker Desktop](https://www.docker.com/get-started) to run Ghostwriter containers on your local machine. You will need Docker version >=20 for the Alpine Linux images for Ghostwriter. Run `docker version` to check your installation. Look at the `Version` value for the client and server. You will need Docker Compose version >=1.26 to support the compose files. Run `docker compose version` to check your installation. Older versions of Docker Compose will try to build the containers and run into issues while parsing the configuration file with Docker's older DotEnv file parser—specifically, issues with quotations. Compose versions below v1.26 will parse configurations like port numbers literally and cause errors when Docker tries to bind a port like `"8000"`(with the quotes). As of v1.26, Compose uses Python's DotEnv parser, which understands quotations. Older installations of Docker Compose use `docker-compose` as the command. If you have `docker-compose` in your PATH instead of `docker compose`, consider upgrading to the latest version. ```log Checking Docker Version theme={"system"} $ docker compose version Docker Compose version v2.23.3 # Good; >=1.26.x $ docker version Client: Version: 25.0.3 # Good; >=20 « snip » ``` Download the latest release of **[Ghostwriter CLI](https://github.com/GhostManager/ghostwriter_cli/releases)** for your operating system (macOS, Linux, or Windows) and architecture (AMD64 or ARM). Ghostwriter CLI is a utility that makes it easy to install and manage Ghostwriter and its containers on your machine. You can also use the command line to download the software. ```bash Linux theme={"system"} wget https://github.com/GhostManager/ghostwriter_cli/releases/latest/download/ghostwriter-cli-linux-amd64.tar.gz ``` ```powershell Windows PowerShell theme={"system"} curl.exe -L -o "$env:USERPROFILE\Downloads\ghostwriter-cli-windows-amd64.zip" https://github.com/GhostManager/ghostwriter_cli/releases/latest/download/ghostwriter-cli-windows-amd64.zip ``` ```bash Mac theme={"system"} curl -L -o ghostwriter-cli-darwin-arm64.tar.gz https://github.com/GhostManager/ghostwriter_cli/releases/latest/download/ghostwriter-cli-darwin-arm64.tar.gz ``` More information about Ghostwriter CLI is detailed in the [Managing the Server with Ghostwriter CLI](/getting-started/managing-the-server-with-ghostwriter-cli) section. Change to the directory where you downloaded the file and unpack it. ```bash Linux theme={"system"} tar -xvzf ghostwriter-cli-linux-amd64.tar.gz ``` ```powershell Windows PowerShell theme={"system"} cd "$env:USERPROFILE\Downloads"; tar -xf ghostwriter-cli-windows-amd64.zip ``` ```bash Mac theme={"system"} tar -xvzf ghostwriter-cli-darwin-arm64.tar.gz ``` In your terminal or PowerShell, navigate to the directory where you unpacked the Ghostwriter CLI and install Ghostwriter. ```bash Linux theme={"system"} ./ghostwriter-cli install ``` ```powershell Windows PowerShell theme={"system"} .\ghostwriter-cli install ``` ```bash Mac theme={"system"} ./ghostwriter-cli install ``` During install, Ghostwriter will generate some TLS certificates and download the latest Docker file. You can find these files in your operating system's XDG data file directory: * Linux: `~/.local/share/ghostwriter/` * macOS: `~/Library/Application Support/ghostwriter/` * Windows: `%LOCALAPPDATA%/ghostwriter/` Keep your terminal open until you see the randomly generated password displayed. Save this password for the next step. ```bash theme={"system"} [+] Ghostwriter is ready to go! [+] You can log in as `admin` with this password: ``` If you lose the password, retrieve it with: ```bash Linux theme={"system"} ./ghostwriter-cli config get ADMIN_PASSWORD ``` ```powershell Windows PowerShell theme={"system"} .\ghostwriter-cli config get ADMIN_PASSWORD ``` ```bash Mac theme={"system"} ./ghostwriter-cli config get ADMIN_PASSWORD ``` This password is only used for creating the account. You can change it in the admin panel after logging into Ghostwriter. You can also change any other part of the user profile. Changing the password in the config DOES NOT affect the installation. It's only stored in the config to make it easy for you to retrieve the original password. In a browser, go to [https://localhost/](https://localhost/) and log in with the `admin` username and the randomly generated password. Ghostwriter will start listening on port 443 for production use. For development environments, you can use `--mode local-dev` and Ghostwriter will start on localhost (127.0.0.1) and listen on port 8000. `./ghostwriter-cli install --mode local-dev` A development environment is best if you want to change Ghostwriter's codebase or test functionality. Debug logging is enabled, which makes it easier to troubleshoot. The `dev` installation does not use TLS, so it skips creating certificates. Read on to customize your installation. There is more information below in [Customizing Your Installation](/getting-started/quickstart#customizing-your-installation). The `install` command will take care of everything necessary to create a production environment for you. That command performs the following actions: * Sets up the default server configuration * Generates TLS certificates for the server * Pulls the latest pre-built container images * Creates a default *admin* user with a randomly generated password Ghostwriter will create self-signed TLS/SSL certificates. If you'd like to use your signed certificates, do that now to make things easier. If you don't have them ready, you can install them later. ### Customizing Your Installation You may wish to change some of the configuration options. The following sections outline common customizations. If you make changes to the configuration, restart Ghostwriter for the changes to take effect: ```log Bouncing Containers theme={"system"} ./ghostwriter-cli containers down ./ghostwriter-cli containers up ``` #### Customizing the Date Format The default format is `d M Y` which formats dates like so: *3 Jun 2022* This format is the default used in parts of the user interface and reports. You can change the date format with this command: `./ghostwriter-cli config set date_format "d M Y"` When you set `DATE_FORMAT` use Django's format string values: [https://docs.djangoproject.com/en/4.0/ref/templates/builtins/#std:templatefilter-date](https://docs.djangoproject.com/en/4.0/ref/templates/builtins/#std:templatefilter-date) #### Using Your Certificates You can use your own TLS/SSL certificates for Ghostwriter. To swap in your certificate package: 1. Name the keypair files *ghostwriter.key* and *ghostwriter.crt* 2. Name the Diffie-Helman Parameters file *dhparam.pem* 3. Place all three files inside the *ssl* directory created during installation (e.g., `~/.local/share/ghostwriter/ssl`, `~/Library/Application Support/ghostwriter/ssl`, or `%LOCALAPPDATA%/ghostwriter/ssl`) Your certificate will likely have a new hostname, so continue to the next section to complete the customization of your domain name. #### Customizing the Domain Name or IP Address Ghostwriter explicitly checks the hostname against a list of allowed hosts to avoid potential exposure to [HTTP Host header attacks](https://portswigger.net/web-security/host-header). To access Ghostwriter with your custom domain name or server IP address, you must tell the server to allow new IP addresses or hostnames. To allow a new IP address or hostname, run this command: `./ghostwriter-cli config allowhost ` If you are setting up a new domain accompanied by a TLS certificate, update the Nginx hostname to match your new certificate and domain name: `./ghostwriter-cli config set NGINX_HOST ` You can use `config disallowhost` to remove a host you have added to the list. While **not** recommended, a wildcard (`*`) will work, but only `*`. A `*` will allow any hostname or IP address. Anything like `*.myserver.local` or `192.168.10.*` will not work to allow a host. #### Configuring Access Through a Web Proxy Similar to the HTTP `Host` header protections, Ghostwriter also checks the `Origin` and `Referer` headers. If you are accessing Ghostwriter through a proxy, configure Ghostwriter to trust the proxy with this command: `./ghostwriter-cli config trustorigin ` You can use `config distrustorigin` to remove a proxy you have added to the list. ### Adding More Users You may create users using the admin panel or ask users to sign-up using `/accounts/signup`. Filling out a complete profile is recommended. Ghostwriter can use full names for displaying user actions and filling in report templates. Django usernames are *case-sensitive*, so all lowercase is recommended to avoid confusion later. ## Migrating TOTP Secrets from Legacy Installations If you are upgrading from an older version of Ghostwriter that used `allauth-2fa` for two-factor authentication, you'll need to migrate your TOTP secrets to the new format: ```log Migrate TOTP Secrets theme={"system"} $ ./ghostwriter-cli migrate_totp ``` # Updating Ghostwriter Source: https://www.ghostwriter.wiki/getting-started/updating-ghostwriter How to check for updates and what to do when one is available ## Checking for Updates You can check for updates with Ghostwriter CLI and the `version` command. The output will look something like this: ```bash Checking for Updates theme={"system"} $ ./ghostwriter-cli version [+] Fetching latest version information: [+] Checking the status of Docker and the Compose plugin... Ghostwriter CLI Local Version v0.1.0 Latest Release v0.3.0 Ghostwriter Local Version v6.2.3 Latest Release v6.2.3 Download the latest version of Ghostwriter CLI at: https://github.com/GhostManager/Ghostwriter_CLI/releases/tag/v0.3.0 ``` The command will take a moment to run as Ghostwriter CLI requests the latest release number from GitHub. The latest version number will not be displayed if you do not have a network connection. If your version number and release date are older than the reported latest release, you may want to update. Check the [Ghostwriter CHANGELOG](https://github.com/GhostManager/Ghostwriter/blob/master/CHANGELOG.md) to see what has changed to determine if now is the right time to update for you. ## Installing Updates Updating Ghostwriter is as easy as running the `update` command to pull the latest published images. Updates are generally straightforward, but you should **strongly** consider taking a snapshot of your host server if anything goes wrong. There is always a chance something like a Python library may not install correctly, and you don't have the time to address it on the spot. You will thank yourself if you can restore a snapshot and try again later. To perform an update: ```bash Linux theme={"system"} ./ghostwriter-cli update ``` ```powershell Windows PowerShell theme={"system"} .\ghostwriter-cli update ``` ```bash Mac theme={"system"} ./ghostwriter-cli update ``` The `update` command pulls the latest published images. You can also use the `--version` tag to specify a version. ```bash Linux theme={"system"} ./ghostwriter-cli update --version v6.2.3 ``` ```powershell Windows PowerShell theme={"system"} .\ghostwriter-cli update --version v6.2.3 ``` ```bash Mac theme={"system"} ./ghostwriter-cli update --version v6.2.3 ``` The `--version` flag does not support *downgrading* your version. # Welcome to Ghostwriter Source: https://www.ghostwriter.wiki/home Introducing the SpecterOps project management and reporting platform Ghostwriter Ghostwriter is an open-source platform designed to enhance offensive security operations by simplifying report writing, asset tracking, and assessment management. It offers tools for managing clients, creating a reusable findings library, and organizing the infrastructure and domains utilized during assessments. With its powerful reporting engine, Ghostwriter includes comprehensive collaborative writing features and customizable report templates, allowing teams to produce polished deliverables with minimal manual effort. Ghostwriter comes equipped with "enterprise-level" features, such as role-based access controls, single sign-on authentication, and multi-factor authentication. Additionally, it integrates with tools like Mythic C2 and Cobalt Strike to enable automatic activity logging. These capabilities make Ghostwriter an ideal centralized and collaborative environment for red teams and consultants to efficiently plan, execute, and document their assessments. The platform effectively tracks and manages client and project information, covert infrastructure assets (such as servers and domain names), finding templates, report templates, evidence files, and more. This data is accessible to Ghostwriter's reporting engine, which generates comprehensive Word (DOCX) reports using Jinja2 templating and your customized report templates. Ghostwriter can also produce reports in XLSX, PPTX, and JSON formats. Furthermore, you can leverage Ghostwriter's GraphQL API to integrate custom project management, reporting workflows, and external tools into the platform. This site will always have the most up-to-date documentation on how to install and use Ghostwriter. To learn more about Ghostwriter, check out this [article](https://posts.specterops.io/introducing-ghostwriter-part-1-61e7bd014aff). Ghostwriter is entirely free and open-source (FOSS) software. All the code is available on [GitHub](https://github.com/GhostManager/Ghostwriter). ### License Ghostwriter is licensed under the **BSD-3** license. ```markdown LICENSE.md theme={"system"} Copyright (c) 2025, Christopher Maddalena All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Ghostwriter nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` # The Workflow Source: https://www.ghostwriter.wiki/workflow/basic-usage Understanding the basic workflow of Ghostwriter ## The Mission Statement Ghostwriter's primary goal is bringing all of your operational data together in one place and create relationships. A starting point is needed to accomplish this goal. For Ghostwriter that starting point is a client. ### Basic Workflow The basic workflow looks like this: 1. Create a new client, or open an existing client 2. Review points of contact for the client and add/edit as needed 3. Create a project under the client 4. Checkout servers and domain names for the new project 5. Create the links between domain names, subdomains, and servers 6. Create an oplog for the project and configure [automatic syncing](/features/operation-logs/setting-up-automated-logging) (if C2 is used) At this stage your project proceeds until it's time to begin noting observations: 1. Create one or more reports for the new project 2. Browse the database of findings/observations and applicable entries to the report 3. Add affected hosts/users, add evidence files, and customize the finding as needed 4. Return to step 2 5. Perform peer review/QA of all findings and project details prior to report generation 6. Upload a report template (optional) 7. Generate a reporting document (docx, pptx, xlsx, json, etc) That's all there is to the basic procedures and their required order of precedence. ### End of Project Workflow At the end of a project a project manager or assessment lead should mark a project as complete. This is done by clicking the *In Progress* toggle below the project's name on the project's detail page. Marking a project as complete begins a 90-day countdown to archiving. If the archive task has been configured (see [Background Tasks](/features/background-tasks)), Ghostwriter will perform a daily check to see if any complete projects are 90 days old (or older) and archive them. The default is 90 days, but this can be adjusted in the `tasks.py` file. Archiving involves the following actions: * Mark all reports under the project to **Complete** (if they were not marked as such already) * Mark all reports under the project as Archived * Generate all report types * Bundle all reports and evidence files into a zip file * Add a record to the `Archive` model for the client and project with the report archive file * Mark the project as archived * Delete all report data The archive file are available for download under `/reporting/reports/archive`. You can leave them or perform any actions required by your company's data retention policies (e.g. download the archive and then delete it from Ghostwriter). Once archived, the project and reports can no longer be edited, so they now exist only as a historical record. # Finding Edit & Review Workflow Source: https://www.ghostwriter.wiki/workflow/basic-usage/finding-review-workflow The supported workflow for reviewing findings added to reports ## Local Findings Findings added to a report are considered "local" to that report. Editing these findings *have no effect* on the "master" record in the findings library. ### Finding Assignments When a finding is added to a report the **Assigned Operator** is automatically set to the current user. The assignee is the **Owner** of that finding. If the current user owns the finding they will see a bold red **You** in that column. Findings owned by other users are represented by a grey username. Findings assigned to a user will appear on that user's dashboard under `/home` when they login. ### Finding Status Each local finding has a **Status** that defaults to **Needs Editing**. Once the owner has finished editing the finding and attached evidence files they click **Mark as Complete** from the dropdown menu under **Options**. This changes the status to **Ready for Review**, a sign to the assessment lead that the finding is complete and can be reviewed for finalizing the write-up. If the reviewer believes the finding needs more work, the **Flag for Editing** option will reverse the status. This is a preliminary version of the review workflow. Future versions will enable reviewers to leave comments, sign-off on findings, and track changes.