]> git.ipfire.org Git - ipfire-2.x.git/commitdiff
suricata-report-generator: Add all alerts in full detail
authorMichael Tremer <michael.tremer@ipfire.org>
Thu, 7 Aug 2025 16:32:13 +0000 (17:32 +0100)
committerMichael Tremer <michael.tremer@ipfire.org>
Wed, 3 Sep 2025 17:42:01 +0000 (18:42 +0100)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
config/suricata/suricata-report-generator

index bcbfd6b73d0b680794465f592da2fa993cc4ca97..2df7c7ef6cb3876b1e55d21d461ed05ca57e2542 100644 (file)
@@ -21,6 +21,7 @@
 
 import argparse
 import calendar
+import collections
 import datetime
 import logging
 import reportlab
@@ -29,7 +30,7 @@ import reportlab.platypus
 import socket
 import sqlite3
 
-from reportlab.lib.units import cm
+from reportlab.lib.units import cm, mm
 
 log = logging.getLogger("suricata-report-generator")
 log.setLevel(logging.DEBUG)
@@ -37,6 +38,16 @@ log.setLevel(logging.DEBUG)
 # i18n
 _ = lambda x: x
 
+def row_factory(cursor, row):
+       """
+               This is a custom row factory that makes all fields accessible as attributes.
+       """
+       # Create a new class with all fields
+       cls = collections.namedtuple("Row", [column for column, *args in cursor.description])
+
+       # Parse the row data
+       return cls._make(row)
+
 class ReportGenerator(object):
        """
                This is the main class that handles all the things...
@@ -46,6 +57,7 @@ class ReportGenerator(object):
 
                # Open the database
                self.db = sqlite3.connect(path)
+               self.db.row_factory = row_factory
 
                # Load a default stylesheet for our document
                self.styles = reportlab.lib.styles.getSampleStyleSheet()
@@ -110,6 +122,9 @@ class ReportGenerator(object):
                # Create a new PDF document
                doc = reportlab.platypus.SimpleDocTemplate(
                        output, pagesize=reportlab.lib.pagesizes.A4,
+
+                       # Decrease the margins
+                       leftMargin=5 * mm, rightMargin=5 * mm, topMargin=10 * mm, bottomMargin=15 * mm,
                )
 
                # Collect everything that should go on the document
@@ -118,8 +133,21 @@ class ReportGenerator(object):
                # Create the title page
                self._make_titlepage(elements, date_start, date_end)
 
+               # Add detailed alerts
+               self._make_alerts(elements, date_start, date_end, width=doc.width)
+
                # Render the document
-               doc.build(elements)
+               doc.build(elements, onLaterPages=self._make_page_number)
+
+       def _make_page_number(self, canvas, doc):
+               # Fetch the current page number
+               number = canvas.getPageNumber()
+
+               # Set the font
+               canvas.setFont(self.styles["Normal"].fontName, 9)
+
+               # Write the page number to the right hand bottom
+               canvas.drawRightString(200 * mm, 10 * mm, _("Page %s") % number)
 
        def _make_titlepage(self, elements, date_start, date_end):
                """
@@ -178,6 +206,141 @@ class ReportGenerator(object):
                        reportlab.platypus.PageBreak(),
                )
 
+       def _make_alerts(self, elements, date_start, date_end, **kwargs):
+               """
+                       Called to add all alerts in the date range with all their detail.
+               """
+               date = date_start
+
+               while date <= date_end:
+                       self._make_alerts_by_date(elements, date, **kwargs)
+
+                       # Move on to the next day
+                       date += datetime.timedelta(days=1)
+
+       def _make_alerts_by_date(self, elements, date, *, width):
+               log.debug("Rendering alerts for %s..." % date)
+
+               # Fetch the alerts
+               c = self.db.execute("""
+                       SELECT
+                               id,
+                               datetime(timestamp, 'unixepoch', 'localtime') AS timestamp,
+
+                               -- Basic Stuff
+                               (event ->> '$.src_ip') AS source_address,
+                               (event ->> '$.src_port') AS source_port,
+                               (event ->> '$.dest_ip') AS destination_address,
+                               (event ->> '$.dest_port') AS destination_port,
+                               (event ->> '$.proto') AS protocol,
+                               (event ->> '$.icmp_code') AS icmp_code,
+                               (event ->> '$.icmp_type') AS icmp_type,
+
+                               -- Alert Stuff
+                               (event ->> '$.alert.category') AS alert_category,
+                               (event ->> '$.alert.signature') AS alert_signature,
+                               (event ->> '$.alert.signature_id') AS alert_signature_id,
+                               (event ->> '$.alert.severity') AS alert_severity,
+                               (event ->> '$.alert.action') AS alert_action,
+                               (event ->> '$.alert.gid') AS alert_gid,
+                               (event ->> '$.alert.rev') AS alert_rev
+                       FROM
+                               alerts
+                       WHERE
+                               date(timestamp, 'unixepoch', 'localtime') = ?
+                       ORDER BY
+                               timestamp ASC,
+                               id ASC
+               """, (date.isoformat(),))
+
+               # Start the table with the header
+               rows = [
+                       (_("Time"), _("Signature"), _("Protocol"), _("Source / Destination"))
+               ]
+
+               while True:
+                       row = c.fetchone()
+                       if row is None:
+                               break
+
+                       print(row)
+
+                       # Parse the timestamp
+                       t = datetime.datetime.strptime(row.timestamp, "%Y-%m-%d %H:%M:%S")
+
+                       # Append the row
+                       rows.append((
+                               t.strftime("%H:%M:%S"),
+                               "%s %s\n[%s:%s:%s] - %s" % (
+                                       "*" * row.alert_severity,
+                                       row.alert_signature,
+                                       row.alert_gid,
+                                       row.alert_signature_id,
+                                       row.alert_rev,
+                                       row.alert_category,
+                               ),
+                               row.protocol,
+                               "%s:%s\n%s:%s" % (
+                                       row.source_address, (row.source_port or row.icmp_code),
+                                       row.destination_address, (row.destination_port or row.icmp_type),
+                               ),
+                       ))
+
+               # Skip if we have found no data
+               if len(rows) == 1:
+                       log.debug("Skipping %s, because we don't have any data" % date)
+                       return
+
+               # Add a headline
+               elements.append(
+                       reportlab.platypus.Paragraph(
+                               _("Alerts from %s") % date.strftime("%A, %d %B %Y"),
+                               self.styles["Heading2"],
+                       )
+               )
+
+               # Create the table
+               table = reportlab.platypus.Table(rows,
+                       # Set the widths of the rows
+                       colWidths=(
+                               width * 0.1, width * 0.6, width * 0.1, width * 0.2,
+                       ),
+
+                       # Repeat the header after a page break
+                       repeatRows=1,
+               )
+
+               # Style the table
+               table.setStyle(
+                       reportlab.platypus.TableStyle((
+                               # Make the grid slightly grey
+                               ("GRID", (0, 0), (-1, -1), 0.25, reportlab.lib.colors.grey),
+
+                               # Align all content to the top left corners of the cells
+                               ("ALIGN", (0, 0), (-1, -1), "LEFT"),
+                               ("ALIGN", (0, 0), (0, -1), "CENTER"),
+                               ("ALIGN", (2, 0), (2, -1), "CENTER"),
+                               ("VALIGN", (0, 0), (-1, -1), "TOP"),
+
+                               # Chose a much smaller font size
+                               ("FONTSIZE", (0, 0), (-1, -1), 8),
+
+                               # Alternate the background colours of the rows
+                               ("ROWBACKGROUNDS", (0, 1), (-1, -1), [
+                                       reportlab.lib.colors.white,
+                                       reportlab.lib.colors.lightgrey,
+                               ]),
+                       )),
+               )
+
+               # Append the table to the output
+               elements.append(table)
+
+               # End the page
+               elements.append(
+                       reportlab.platypus.PageBreak(),
+               )
+
 
 def setup_logging(loglevel=logging.INFO):
        log.setLevel(loglevel)