Skip to content

Commit

Permalink
Migrate eventstream to use same evidence expansion/styling as cobalt …
Browse files Browse the repository at this point in the history
…strike for web & PDF
  • Loading branch information
neonbunny committed Nov 11, 2024
1 parent 778fa30 commit e2147c6
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 163 deletions.
18 changes: 18 additions & 0 deletions event_tracker/static/css/event_table.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.description ~ div, .input ~ div {
margin-top: 0.5rem;
margin-bottom: 0;
}

.input::before {
content: "\00BB ";
}

.input, .output {
display: block;
font-family: monospace;
white-space: pre-wrap;
}

.output {
font-style: italic;
}
88 changes: 88 additions & 0 deletions event_tracker/static/scripts/event_table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
pdfMake.preserveLeadingSpaces = true

function pdfExportCustomize(doc, config, dt) {
pdfMake.fonts = {
RobotoMono: {
normal: eventTableConfig.monospaceFontURL,
},
Roboto: {
normal: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.66/fonts/Roboto/Roboto-Regular.ttf',
bold: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.66/fonts/Roboto/Roboto-Medium.ttf',
},
}

doc.styles['terminal'] = {
font: 'RobotoMono',
bold: false
}

for (const row of doc.content[1].table.body) {
let description = row[eventTableConfig.descriptionColumn] // column of each row that should be "Description"
const parts = description.text.split("{monospace}")
if (parts.length > 1) {
// Attach 'terminal' style to everything after the first "\n\n"
description.text = [parts[0], parts[0].length ? "\n\n" : "", {
text: parts.slice(1).join("\n\n"),
style: 'terminal',
preserveLeadingSpaces: true
}]
}
}

// Ensure the main text column doesn't stretch when given long content
doc.content[1].table.widths = Array(eventTableConfig.totalColumns).fill("auto")
doc.content[1].table.widths[eventTableConfig.descriptionColumn] = 400
// Sprinkle in some corporate branding
doc.footer = function (currentPage, pageCount) {
return [
{
canvas: [
{type: 'line', x1: 40, y1: 0, x2: 800, y2: 0, lineWidth: 0.5, lineColor: '#242C7A'}
]
},
{
columns: [
currentPage.toString() + ' / ' + pageCount,
{svg: eventTableConfig.brandingSVG, alignment: 'center'},
{text: eventTableConfig.brandingText, alignment: 'right'},
],
margin: [40, 10],
},
]
}
}

function pdfExportAction (e, dt, node, config, cb) {
let outer_dt = dt;
let outer_config = config;
let orig_len = dt.page.len();
let outer_cb = cb;

doExport = function (e, _dt, node, _config, cb) {
// Deregister the event handler
dt.off('draw', doExport);
// Trigger the print action
$.fn.dataTable.ext.buttons.pdfHtml5.action.call(outer_dt.button(), e, outer_dt, node, outer_config, outer_cb);
// Redraw the table at the original page size
dt.page.len(orig_len).draw();
}

// Register an event handler to print the table once all the data is loaded
dt.on( 'draw', doExport )
// Trigger a non-paginated table draw
dt.page.len(-1).draw();
}

function descriptionRender (data, type, row) {
if (type === "export") {
// When exporting to PDF, before the HTML is stripped and passed to pdfMake,
// add a double new line to show where the <div>s end. Used by customize function above.
return data.split(/<div class=['"](?:out|in)put['"]>/).join("{monospace}")
} else {
return data;
}
}

function tableDrawCallback (settings) {
$('.output').expander({slicePoint: 200, normalizeWhitespace: false, detailPrefix: '',});
}
119 changes: 16 additions & 103 deletions event_tracker/templates/cobalt_strike_monitor/archive_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@

<script src="{% static "/scripts/jquery.expander.js" %}"></script>

<link rel="stylesheet" href="{% static "css/event_table.css"%}">
<script src="{% static "scripts/event_table.js" %}"></script>
<script nonce="{{request.csp_nonce}}">
let eventTableConfig = {
monospaceFontURL: '{{request.scheme}}://{{request.META.HTTP_HOST}}{% static "fonts/RobotoMono-Regular.ttf" %}',
brandingSVG: '{{REPORT_FOOTER_IMAGE}}',
brandingText: '{{REPORT_FOOTER_TEXT}}',
totalColumns: 6,
descriptionColumn: 3,
}
</script>

<style nonce="{{request.csp_nonce}}">
.fa-ul {
margin-left: 30px; margin-bottom: 0px
Expand All @@ -38,25 +50,6 @@
padding-top: 0.5em;
padding-bottom: 1em;
}

.description ~ div, .input ~ div {
margin-top: 0.5rem;
margin-bottom: 0;
}

.input::before {
content: "\00BB ";
}

.input, .output {
display: block;
font-family: monospace;
white-space: pre-wrap;
}

.output {
font-style: italic;
}
</style>
{% endblock head %}

Expand Down Expand Up @@ -125,78 +118,10 @@
},
{
text: 'Export PDF',
action: function (e, dt, node, config, cb) {
let outer_dt = dt;
let outer_config = config;
let orig_len = dt.page.len();
let outer_cb = cb;

doExport = function (e, _dt, node, _config, cb) {
// Deregister the event handler
dt.off('draw', doExport);
// Trigger the print action
$.fn.dataTable.ext.buttons.pdfHtml5.action.call(outer_dt.button(), e, outer_dt, node, outer_config, outer_cb);
// Redraw the table at the original page size
dt.page.len(orig_len).draw();
}

// Register an event handler to print the table once all the data is loaded
dt.on( 'draw', doExport )
// Trigger a non-paginated table draw
dt.page.len(-1).draw();
},
action: pdfExportAction,
extend: 'pdfHtml5', // Required to pull in default config
orientation: "landscape",
customize: function (doc, config, dt) {
pdfMake.fonts = {
RobotoMono: {
normal: '{{request.scheme}}://{{request.META.HTTP_HOST}}{% static "fonts/RobotoMono-Regular.ttf" %}',
},
Roboto: {
normal: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.66/fonts/Roboto/Roboto-Regular.ttf',
bold: 'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.66/fonts/Roboto/Roboto-Medium.ttf',
},
}

doc.styles['terminal'] = {
font: 'RobotoMono',
bold: false
}

for (const row of doc.content[1].table.body) {
let description = row[3] // the 4th (i.e. [3]) column of each row should be "Description"
const parts = description.text.split("\n\n")
if (parts.length > 1) {
// Attach 'terminal' style to everything after the first "\n\n"
description.text = [parts[0], "\n\n", {
text: parts.slice(1).join("\n\n"),
style: 'terminal',
preserveLeadingSpaces: true
}]
}
}

// Ensure the main text column doesn't stretch when given long content
doc.content[1].table.widths=['auto','auto','auto',400,'auto','auto'];
// Sprinkle in some corporate branding
doc.footer = function (currentPage, pageCount) {
return [
{
canvas: [
{ type: 'line', x1: 40, y1: 0, x2: 800, y2: 0, lineWidth: 0.5, lineColor: '#242C7A'}
]
},
{
columns: [
currentPage.toString() + ' / ' + pageCount,
{svg: '{{REPORT_FOOTER_IMAGE}}', alignment: 'center'},
{text: '{{REPORT_FOOTER_TEXT}}', alignment: 'right'},
],
margin: [40, 10],
},
]
}
},
customize: pdfExportCustomize,
title: "Cobalt Strike Logs",
exportOptions: {
stripNewlines: false,
Expand All @@ -213,23 +138,11 @@
{ searchBuilderType: "moment-{% datetime_format_moment %}" },
{ orderable: false },
{ orderable: false },
{
render: function (data, type, row) {
if (type === "export") {
// When exporting to PDF, before the HTML is stripped and passed to pdfMake,
// add a double new line to show where the <div>s end. Used by customize function above.
return data.split("</div>").join("</div>\n\n")
} else {
return data;
}
}
},
{ render: descriptionRender },
null,
{ orderable: false },
],
drawCallback: function (settings) {
$('.output').expander({slicePoint: 200, normalizeWhitespace: false, detailPrefix: '',});
}
drawCallback: tableDrawCallback
} )
})
</script>
Expand Down
66 changes: 21 additions & 45 deletions event_tracker/templates/event_tracker/eventstream_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@
{% include "base/external-libs/jquery.html" %}
{% include "base/external-libs/datatables-pdfexport.html" %}

<script src="{% static "/scripts/jquery.expander.js" %}"></script>

<link rel="stylesheet" href="{% static "css/event_table.css"%}">
<script src="{% static "scripts/event_table.js" %}"></script>
<script nonce="{{request.csp_nonce}}">
let eventTableConfig = {
monospaceFontURL: '{{request.scheme}}://{{request.META.HTTP_HOST}}{% static "fonts/RobotoMono-Regular.ttf" %}',
brandingSVG: '{{REPORT_FOOTER_IMAGE}}',
brandingText: '{{REPORT_FOOTER_TEXT}}',
totalColumns: 7,
descriptionColumn: 3,
}
</script>

<style nonce="{{request.csp_nonce}}">
.fa-ul {
margin-left: 30px; margin-bottom: 0px
Expand Down Expand Up @@ -130,54 +144,15 @@
},
{
text: 'Export PDF',
action: function (e, dt, node, config, cb) {
let outer_dt = dt;
let outer_config = config;
let orig_len = dt.page.len();
let outer_cb = cb;

doExport = function (e, _dt, node, _config, cb) {
// Deregister the event handler
dt.off('draw', doExport);
// Trigger the print action
$.fn.dataTable.ext.buttons.pdfHtml5.action.call(outer_dt.button(), e, outer_dt, node, outer_config, outer_cb);
// Redraw the table at the original page size
dt.page.len(orig_len).draw();
}

// Register an event handler to print the table once all the data is loaded
dt.on( 'draw', doExport )
// Trigger a non-paginated table draw
dt.page.len(-1).draw();
},
action: pdfExportAction,
extend: 'pdfHtml5', // Required to pull in default config
orientation: "landscape",
customize: function (doc, config, dt) {
// Ensure the main text column doesn't stretch when given long content
doc.content[1].table.widths=['auto','auto','auto',400,'auto','auto','auto'];
// Sprinkle in some corporate branding
doc.footer = function (currentPage, pageCount) {
return [
{
canvas: [
{ type: 'line', x1: 40, y1: 0, x2: 800, y2: 0, lineWidth: 0.5, lineColor: '#242C7A'}
]
},
{
columns: [
currentPage.toString() + ' / ' + pageCount,
{svg: '{{REPORT_FOOTER_IMAGE}}', alignment: 'center'},
{text: '{{REPORT_FOOTER_TEXT}}', alignment: 'right'},
],
margin: [40, 10],
},
]
}
},
customize: pdfExportCustomize,
title: "EventStream Logs",
exportOptions: {
columns: [0,1,2,3,4,5,6],
stripNewlines: false
stripNewlines: false,
orthogonal: 'export' // Force a fresh call to the render function below passing in "export" as the type
}
},
],
Expand All @@ -195,11 +170,12 @@
{ searchBuilderType: "moment-{% datetime_format_moment %}" },
{ orderable: false },
{ orderable: false },
null,
{ render: descriptionRender },
null,
{ orderable: false },
{ orderable: false },
]
],
drawCallback: tableDrawCallback
} )
})
</script>
Expand Down
10 changes: 7 additions & 3 deletions event_tracker/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1054,10 +1054,14 @@ def render_column(self, row, column):
dummy_context = Context(host=row.target_host, user=row.target_user, process=row.target_process)
return dummy_context.get_visible_html()
elif column == 'description':
description = row.description
result = ""
if row.description:
result = f"<div class='description'>{row.description}</div>"

if row.raw_evidence:
description += f'<pre class="mt-3 mb-0"><code>{ breakonpunctuation(escape(row.raw_evidence)) }</code></pre>'
return description
result += f"<div class='output'>{escape(row.raw_evidence)}</div>"

return result
elif column == 'additional_data' and row.additional_data:
additional_data_dict = json.loads(row.additional_data)
escaped_dict = {}
Expand Down
Loading

0 comments on commit e2147c6

Please sign in to comment.