Skip to content
Permalink
Browse files

Merge pull request #43 from chrismaddalena/master

Reporting and UI Improvements
  • Loading branch information
chrismaddalena committed Nov 15, 2019
2 parents d472838 + 0f5a3c8 commit 3f03aa54ffdaa36c7a3683bb50608fefdd370b6d
@@ -36,4 +36,10 @@ NAMECHEAP_API_KEY=
NAMECHEAP_USERNAME=
NAMECHEAP_API_USERNAME=
CLIENT_IP=
NAMECHEAP_PAGE_SIZE=100
NAMECHEAP_PAGE_SIZE=100

# Cloud Services
# ------------------------------------------------------------------------------
AWS_KEY=
AWS_SECRET=
DO_API_KEY=
@@ -65,4 +65,10 @@ NAMECHEAP_API_KEY=
NAMECHEAP_USERNAME=
NAMECHEAP_API_USERNAME=
CLIENT_IP=
NAMECHEAP_PAGE_SIZE=100
NAMECHEAP_PAGE_SIZE=100

# Cloud Services
# ------------------------------------------------------------------------------
AWS_KEY=
AWS_SECRET=
DO_API_KEY=
@@ -59,6 +59,10 @@ http {
ssl_prefer_server_ciphers on;
resolver 8.8.8.8;

location /media {
alias /app/media;
}

location /admin {
try_files $uri @proxy_to_app;
}
@@ -338,4 +338,11 @@
'namecheap_api_username': env("NAMECHEAP_API_USERNAME", default=None),
'client_ip': env("CLIENT_IP", default=None),
'namecheap_page_size': env("NAMECHEAP_PAGE_SIZE", default="100")
}
}

# Cloud service configuration
CLOUD_SERVICE_CONFIG = {
'aws_key': env("AWS_KEY", default=None),
'aws_secret': env("AWS_SECRET", default=None),
'do_api_key': env("DO_API_KEY", default=None)
}
@@ -66,6 +66,20 @@ def __init__(self, report_queryset, output_path, evidence_path,
self.evidence_path = evidence_path
self.report_queryset = report_queryset

def valid_xml_char_ordinal(self, c):
"""Clean string to make all characters XML compatible for Word documents.
https://stackoverflow.com/questions/8733233/filtering-out-certain-bytes-in-python
"""
codepoint = ord(c)
# Conditions ordered by presumed frequency
return (
0x20 <= codepoint <= 0xD7FF or
codepoint in (0x9, 0xA, 0xD) or
0xE000 <= codepoint <= 0xFFFD or
0x10000 <= codepoint <= 0x10FFFF
)

def generate_json(self):
"""Export report as a JSON dictionary."""
project_name = str(self.report_queryset.project)
@@ -391,19 +405,67 @@ def get_abstract_id():
par._p.get_or_add_pPr().get_or_add_numPr().\
get_or_add_ilvl().val = level

def process_inline_text(self, line, p):
"""Process the provided line for the provided paragraph to handle
nested inline text formatting.
"""
# Split line on spaces and curly brackets
all_words = re.split('{{|}}| ', line)
for word in all_words:
prepared_text = word.strip() + ' '
# Check if word is blank
if not word:
continue
# Determine styling
if (
'inline_code' in word and
'end_inline_code' not in word
):
self.inline_code = True
continue
if 'end_inline_code' in word:
self.inline_code = False
continue
if 'italic' in word and 'end_italic' not in word:
self.italic_text = True
continue
if 'end_italic' in word:
self.italic_text = False
continue
if 'bold' in word and 'end_bold' not in word:
self.bold_text = True
continue
if 'end_bold' in word:
self.bold_text = False
continue
# Write the content
if self.inline_code:
run = p.add_run(prepared_text)
run.style = 'Code (inline)'
else:
run = p.add_run(prepared_text)
if self.italic_text:
font = run.font
font.italic = True
if self.bold_text:
font = run.font
font.bold = True

def process_text(self, text, finding, report_json):
"""Process the provided text from the specified finding to parse
keywords for evidence placement and formatting.
"""
numbered_list = False
bulleted_list = False
code_block = False
inline_code = False
italic_text = False
bold_text = False
self.numbered_list = False
self.bulleted_list = False
self.code_block = False
self.inline_code = False
self.italic_text = False
self.bold_text = False
p = None
prev_p = None
regex = r'\{\{\.(.*?)\}\}'
# Clean text to make it XML compatible
text = ''.join(c for c in text if self.valid_xml_char_ordinal(c))
for line in text.split('\n'):
line = line.strip()
# Perform static replacements
@@ -447,7 +509,7 @@ def process_text(self, text, finding, report_json):
line = line.replace(match, '')
# Handle code blocks
if keyword == 'code_block':
code_block = True
self.code_block = True
if line:
p = self.spenny_doc.add_paragraph(line)
p.style = 'CodeBlock'
@@ -457,63 +519,62 @@ def process_text(self, text, finding, report_json):
p = self.spenny_doc.add_paragraph(line)
p.style = 'CodeBlock'
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
code_block = False
self.code_block = False
continue
# Handle captions - intended to follow code blocks
if keyword == 'caption':
numbered_list = False
bulleted_list = False
self.numbered_list = False
self.bulleted_list = False
p = self.spenny_doc.add_paragraph(
'Figure ',
style='Caption')
self.make_figure(p)
run = p.add_run(' - ' + line)
run = p.add_run(' ' + line)
# Handle lists
if keyword == 'numbered_list':
if line:
p = self.spenny_doc.add_paragraph(
line,
style='Normal')
p = self.spenny_doc.add_paragraph(style='Normal')
self.process_inline_text(line, p)
self.list_number(p, level=0, num=True)
p.paragraph_format.left_indent = Inches(0.5)
numbered_list = True
self.numbered_list = True
if keyword == 'end_numbered_list':
if line:
p = self.spenny_doc.add_paragraph(
line,
style='Normal')
p = self.spenny_doc.add_paragraph(style='Normal')
self.process_inline_text(line, p)
self.list_number(p, level=0, num=True)
p.paragraph_format.left_indent = Inches(0.5)
numbered_list = False
self.numbered_list = False
continue
if keyword == 'bulleted_list':
if line:
p = self.spenny_doc.add_paragraph(
line,
style='Normal')
p = self.spenny_doc.add_paragraph(style='Normal')
self.process_inline_text(line, p)
self.list_number(p, level=0, num=False)
p.paragraph_format.left_indent = Inches(0.5)
bulleted_list = True
self.bulleted_list = True
if keyword == 'end_bulleted_list':
if line:
p = self.spenny_doc.add_paragraph(
line,
style='Normal')
p = self.spenny_doc.add_paragraph(style='Normal')
self.process_inline_text(line, p)
self.list_number(p, level=0, num=False)
p.paragraph_format.left_indent = Inches(0.5)
bulleted_list = False
self.bulleted_list = False
continue
# Continue handling paragraph formatting if active
elif code_block:
p = self.spenny_doc.add_paragraph(line)
elif self.code_block:
p = self.spenny_doc.add_paragraph()
p.style = 'CodeBlock'
self.process_inline_text(line, p)
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
elif numbered_list:
p = self.spenny_doc.add_paragraph(line, style='Normal')
elif self.numbered_list:
p = self.spenny_doc.add_paragraph(style='Normal')
self.process_inline_text(line, p)
self.list_number(p, prev=prev_p, level=0, num=True)
p.paragraph_format.left_indent = Inches(0.5)
elif bulleted_list:
p = self.spenny_doc.add_paragraph(line, style='Normal')
elif self.bulleted_list:
p = self.spenny_doc.add_paragraph(style='Normal')
self.process_inline_text(line, p)
self.list_number(p, level=0, num=False)
p.paragraph_format.left_indent = Inches(0.5)
# Handle keywords wrapped around runs of text inside paragraphs
@@ -526,7 +587,6 @@ def process_text(self, text, finding, report_json):
keyword = match.\
replace('{{.', '').\
replace('}}', '').strip()
# line = line.replace(match, '')
# Check if the keyword references evidence
evidence = False
if 'evidence' in finding:
@@ -592,47 +652,7 @@ def process_text(self, text, finding, report_json):
# Handle keywords that require managing runs
p = self.spenny_doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
# Split line on spaces and curly brackets
all_words = re.split('{{|}}| ', line)
for word in all_words:
prepared_text = word.strip() + ' '
# Check if word is blank
if not word:
continue
# Determine styling
if (
'inline_code' in word and
'end_inline_code' not in word
):
inline_code = True
continue
if 'end_inline_code' in word:
inline_code = False
continue
if 'italic' in word and 'end_italic' not in word:
italic_text = True
continue
if 'end_italic' in word:
italic_text = False
continue
if 'bold' in word and 'end_bold' not in word:
bold_text = True
continue
if 'end_bold' in word:
bold_text = False
continue
# Write the content
if inline_code:
run = p.add_run(prepared_text)
run.style = 'Code (inline)'
else:
run = p.add_run(prepared_text)
if italic_text:
font = run.font
font.italic = True
if bold_text:
font = run.font
font.bold = True
self.process_inline_text(line, p)
else:
p = self.spenny_doc.add_paragraph(line, style='Normal')
# Save the current paragraph for next iteration - needed for lists
@@ -54,8 +54,7 @@ class Meta:
"""Metadata for the model form."""
model = Report
exclude = ('creation', 'last_update',
'created_by', 'project',
'complete')
'created_by', 'complete')

def __init__(self, *args, **kwargs):
"""Override the `init()` function to set some attributes."""
@@ -187,7 +187,8 @@ class Report(models.Model):
project = models.ForeignKey(
'rolodex.Project',
on_delete=models.CASCADE,
null=True)
null=True,
help_text='Select the project tied to this report')
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
@@ -11,10 +11,11 @@
{% csrf_token %}

<!-- General Section -->
<strong><i class="fas fa-book"></i> Report Status</strong>
<strong><i class="fas fa-book"></i> Report Details</strong>
<hr>

{{ form.title|as_crispy_field }}
{{ form.project|as_crispy_field }}

<input type="submit" class="btn btn-primary" value="Save">
<button onclick="window.history.back();" class="btn btn-secondary" type="button">Cancel</button>
@@ -13,6 +13,7 @@
FindingDetailView,
FindingUpdate,
ReportCreate,
ReportCreateWithoutProject,
ReportDelete,
ReportDetailView,
ReportFindingLinkDelete,
@@ -89,6 +90,8 @@
name='report_detail'),
path('reports/<int:pk>/create/', ReportCreate.as_view(),
name='report_create'),
path('reports/create/', ReportCreateWithoutProject.as_view(),
name='report_create_no_project'),
path('reports/<int:pk>/update/', ReportUpdate.as_view(),
name='report_update'),
path('reports/<int:pk>/delete/', ReportDelete.as_view(),
@@ -919,6 +919,7 @@ def get_initial(self):
project.start_date)
return {
'title': title,
'project': project
}

def get_success_url(self):
@@ -931,6 +932,31 @@ def get_success_url(self):
return reverse('reporting:report_detail', kwargs={'pk': self.object.pk})


class ReportCreateWithoutProject(LoginRequiredMixin, CreateView):
"""View for creating new reports. This view defaults to the
report_form.html template. This version applies no default values.
"""
model = Report
form_class = ReportCreateForm

def form_valid(self, form):
"""Override form_valid to perform additional actions on new entries."""
from ghostwriter.rolodex.models import Project
form.instance.created_by = self.request.user
self.request.session['active_report'] = {}
self.request.session['active_report']['title'] = form.instance.title
return super().form_valid(form)

def get_success_url(self):
"""Override the function to return to the new record after creation."""
self.request.session['active_report']['id'] = self.object.pk
self.request.session.modified = True
messages.success(self.request, 'New report was successfully created '
'and is now your active report.',
extra_tags='alert-success')
return reverse('reporting:report_detail', kwargs={'pk': self.object.pk})


class ReportUpdate(LoginRequiredMixin, UpdateView):
"""View for updating existing reports. This view defaults to the
report_form.html template.

0 comments on commit 3f03aa5

Please sign in to comment.
You can’t perform that action at this time.