Skip to content

Commit

Permalink
Merge pull request #342 from GhostManager/hotfix/expand-comapny-info
Browse files Browse the repository at this point in the history
Hotfix: v4.0.1
  • Loading branch information
chrismaddalena authored Sep 27, 2023
2 parents d4ae1ea + 6a6245f commit d4f3999
Show file tree
Hide file tree
Showing 14 changed files with 153 additions and 20 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [4.0.1] - 27 September 2023

### Added

* Added `short_name` and `address` fields to the company information for use in report templates (Closes #339)

### Fixed

* Fixed the activity log export returning incorrect csv files (Fixes #341)

### Changed

* Removed the restriction on backup commands that prevented them from being run on if `postgres` was set as the username (Closes #340)

## [4.0.0] - 20 September 2023

Expand Down
4 changes: 2 additions & 2 deletions VERSION
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
v4.0.0
20 September 2023
v4.0.1
27 September 2023
6 changes: 0 additions & 6 deletions compose/production/postgres/maintenance/backup
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@ source "${working_dir}/_sourced/messages.sh"

message_welcome "Backing up the '${POSTGRES_DB}' database..."


if [[ "${POSTGRES_USER}" == "postgres" ]]; then
message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again."
exit 1
fi

export PGHOST="${POSTGRES_HOST}"
export PGPORT="${POSTGRES_PORT}"
export PGUSER="${POSTGRES_USER}"
Expand Down
5 changes: 0 additions & 5 deletions compose/production/postgres/maintenance/restore
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ fi

message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..."

if [[ "${POSTGRES_USER}" == "postgres" ]]; then
message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again."
exit 1
fi

export PGHOST="${POSTGRES_HOST}"
export PGPORT="${POSTGRES_PORT}"
export PGUSER="${POSTGRES_USER}"
Expand Down
4 changes: 2 additions & 2 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
# 3rd Party Libraries
import environ

__version__ = "4.0.0"
__version__ = "4.0.1"
VERSION = __version__
RELEASE_DATE = "20 September 2023"
RELEASE_DATE = "27 September 2023"

ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent
APPS_DIR = ROOT_DIR / "ghostwriter"
Expand Down
27 changes: 27 additions & 0 deletions ghostwriter/commandcenter/migrations/0016_auto_20230922_1642.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 3.2.19 on 2023-09-22 16:42

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("commandcenter", "0015_alter_reportconfiguration_report_filename"),
]

operations = [
migrations.AddField(
model_name="companyinformation",
name="company_address",
field=models.TextField(
default="14 N Moore St, New York, NY 10013", help_text="Company address to reference in reports"
),
),
migrations.AddField(
model_name="companyinformation",
name="company_short_name",
field=models.CharField(
default="SO", help_text="Abbreviated company name to reference in reports", max_length=255
),
),
]
9 changes: 9 additions & 0 deletions ghostwriter/commandcenter/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,15 @@ class CompanyInformation(SingletonModel):
default="SpecterOps",
help_text="Company name handle to reference in reports",
)
company_short_name = models.CharField(
max_length=255,
default="SO",
help_text="Abbreviated company name to reference in reports",
)
company_address = models.TextField(
default="14 N Moore St, New York, NY 10013",
help_text="Company address to reference in reports",
)
company_twitter = models.CharField(
"Twitter Handle",
max_length=255,
Expand Down
4 changes: 3 additions & 1 deletion ghostwriter/modules/custom_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,14 @@ class CompanyInfoSerializer(CustomModelSerializer):
"""Serialize :model:`commandcenter:CompanyInformation` entries."""

name = serializers.CharField(source="company_name")
short_name = serializers.CharField(source="company_short_name")
address = serializers.CharField(source="company_address")
twitter = serializers.CharField(source="company_twitter")
email = serializers.CharField(source="company_email")

class Meta:
model = CompanyInformation
exclude = ["id", "company_name", "company_twitter", "company_email"]
exclude = ["id", "company_name", "company_short_name", "company_address", "company_twitter", "company_email"]


class EvidenceSerializer(TaggitSerializer, CustomModelSerializer):
Expand Down
2 changes: 2 additions & 0 deletions ghostwriter/modules/linting_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,8 @@
],
"company": {
"name": "SpecterOps",
"short_name": "SO",
"address": "14 N Moore St, New York, NY 10013",
"twitter": "@specterops",
"email": "[email protected]",
},
Expand Down
3 changes: 2 additions & 1 deletion ghostwriter/oplog/templates/oplog/oplog_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,8 @@ <h5 class="modal-title">Edit Entry</h5>
{% comment %} Download the log as a CSV file when the user clicks the "Export Entries" menu item {% endcomment %}
$('#exportEntries').click(function () {
let filename = generateDownloadName('{{ oplog.name }}-log-export-{{ id }}.csv');
download(`/oplog/api/entries?export=csv&&oplog_id={{ oplog.pk }}`, filename);
let export_url = "{% url 'oplog:oplog_export' oplog.pk %}";
download(export_url, filename);
})

{% comment %} Open import form page when user clicks the "Import Entries" menu item {% endcomment %}
Expand Down
5 changes: 3 additions & 2 deletions ghostwriter/oplog/templates/oplog/oplog_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ <h4 style="margin-top: 0;" class="alert-heading">Hey You!</h4>
<span class="icon notification-bell-icon">Active</span>
{% endif %}
</td>
<td class="align-middle js-export-oplog" oplog-id="{{ log.pk }}" oplog-name="{{ log.name }}"><a href="javascript:void(0);" class="btn btn-secondary col-md-12">Export</a></td>
<td class="align-middle js-export-oplog" oplog-id="{{ log.pk }}" oplog-name="{{ log.name }}" export_url="{% url 'oplog:oplog_export' log.id %}"><a href="javascript:void(0);" class="btn btn-secondary col-md-12">Export</a></td>
</tr>
{% endfor %}
</table>
Expand Down Expand Up @@ -96,9 +96,10 @@ <h4 style="margin-top: 0;" class="alert-heading">Hey You!</h4>
$(".js-export-oplog").on("click", function () {
id = $(this).attr('oplog-id')
name = $(this).attr('oplog-name')
export_url = $(this).attr('export_url')
base_name = `${name}-log-export-${id}.csv`
filename = generateDownloadName(base_name);
download(`/oplog/api/entries?export=csv&&oplog_id=${id}`, filename);
download(export_url, filename);
});
</script>
{% endblock %}
39 changes: 39 additions & 0 deletions ghostwriter/oplog/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,3 +442,42 @@ def test_view_uses_correct_ajax_template(self):
response = self.client_mgr.get(self.uri, **{"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"})
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "oplog/snippets/oplogentry_form_inner.html")


class OplogExportViewTests(TestCase):
"""Collection of tests for :view:`oplog.OplogExport`."""

@classmethod
def setUpTestData(cls):
cls.Oplog = OplogFactory._meta.model
cls.OplogEntry = OplogEntryFactory._meta.model

cls.oplog = OplogFactory()
OplogEntryFactory.create_batch(5, oplog_id=cls.oplog)

cls.user = UserFactory(password=PASSWORD)
cls.mgr_user = UserFactory(password=PASSWORD, role="manager")
cls.uri = reverse("oplog:oplog_export", kwargs={"pk": cls.oplog.id})

def setUp(self):
self.client = Client()
self.client_auth = Client()
self.client_mgr = Client()
self.assertTrue(self.client_auth.login(username=self.user.username, password=PASSWORD))
self.assertTrue(self.client_mgr.login(username=self.mgr_user.username, password=PASSWORD))

def test_view_uri_exists_at_desired_location(self):
response = self.client_mgr.get(self.uri)
self.assertEqual(response.status_code, 200)

def test_view_requires_login_and_permissions(self):
response = self.client.get(self.uri)
self.assertEqual(response.status_code, 302)

response = self.client_auth.get(self.uri)
self.assertEqual(response.status_code, 302)

ProjectAssignmentFactory(operator=self.user, project=self.oplog.project)

response = self.client_auth.get(self.uri)
self.assertEqual(response.status_code, 200)
1 change: 1 addition & 0 deletions ghostwriter/oplog/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
),
path("<int:pk>/entries", views.OplogListEntries.as_view(), name="oplog_entries"),
path("import", views.oplog_entries_import, name="oplog_import"),
path("export/<int:pk>", views.OplogExport.as_view(), name="oplog_export"),
]

# URLs for AJAX requests
Expand Down
51 changes: 50 additions & 1 deletion ghostwriter/oplog/views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""This contains all the views used by the Oplog application."""

# Standard Libraries
import csv
import logging

# Django Imports
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseRedirect, JsonResponse
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views.generic import ListView
Expand Down Expand Up @@ -403,3 +404,51 @@ def handle_no_permission(self):

def get_success_url(self):
return reverse("oplog:oplog_entries", args=(self.object.oplog_id.id,))


class OplogExport(RoleBasedAccessControlMixin, SingleObjectMixin, View):
"""Export the :oplog:`oplog.Entries` for an individual :model:`oplog.Oplog` in a csv format."""

model = Oplog

def test_func(self):
return verify_access(self.request.user, self.get_object().project)

def handle_no_permission(self):
messages.error(self.request, "You do not have permission to access that.")
return redirect("oplog:index")

def get(self, *args, **kwargs):
obj = self.get_object()

queryset = obj.entries.all()
opts = queryset.model._meta

response = HttpResponse(
content_type="text/csv",
headers={"Content-Disposition": 'attachment; filename="export.csv"'},
)

writer = csv.writer(response)
field_names = [field.name for field in opts.fields]

# Add the tags field to the list of fields
field_names.append("tags")

# Write the headers to the csv file
writer.writerow(field_names)

for obj in queryset:
values = []
for field in field_names:
# Special case for oplog_id to write the ID of the oplog instead of the object
if field == "oplog_id":
values.append(getattr(obj, field).id)
else:
values.append(getattr(obj, field))
# Special case for tags to write a comma-separated list of tag names
if field == "tags":
values.append(", ".join([tag.name for tag in obj.tags.all()]))
writer.writerow(values)

return response

0 comments on commit d4f3999

Please sign in to comment.