• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cisagov / pe-reports / 5891891328

17 Aug 2023 01:59PM UTC coverage: 33.736% (+7.0%) from 26.737%
5891891328

Pull #565

github

web-flow
Merge 40f073f6e into 998fa208f
Pull Request #565: Update report generator to use reportlab

93 of 477 branches covered (19.5%)

Branch coverage included in aggregate %.

444 of 1023 new or added lines in 8 files covered. (43.4%)

18 existing lines in 5 files now uncovered.

801 of 2173 relevant lines covered (36.86%)

1.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.07
/src/pe_reports/reportlab_generator.py
1
"""Generate a P&E report using a passed data dictionary."""
2
# Standard Python Libraries
3
from hashlib import sha256
5✔
4
import os
5✔
5

6
# Third-Party Libraries
7
import demoji
5✔
8
import numpy as np
5✔
9
from reportlab.lib import utils
5✔
10
from reportlab.lib.colors import HexColor
5✔
11
from reportlab.lib.pagesizes import letter
5✔
12
from reportlab.lib.styles import ParagraphStyle
5✔
13
from reportlab.lib.units import inch
5✔
14
from reportlab.pdfbase import pdfmetrics
5✔
15
from reportlab.pdfbase.ttfonts import TTFont
5✔
16
from reportlab.platypus import (
5✔
17
    HRFlowable,
18
    Image,
19
    KeepTogether,
20
    ListFlowable,
21
    ListItem,
22
    PageBreak,
23
    Paragraph,
24
    Spacer,
25
    Table,
26
    TableStyle,
27
)
28
from reportlab.platypus.doctemplate import (
5✔
29
    BaseDocTemplate,
30
    NextPageTemplate,
31
    PageTemplate,
32
)
33
from reportlab.platypus.flowables import BalancedColumns
5✔
34
from reportlab.platypus.frames import Frame
5✔
35
from reportlab.platypus.tableofcontents import TableOfContents
5✔
36

37
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
5✔
38

39
for font_name, font_filename in [
5✔
40
    ("Franklin_Gothic_Book", "FranklinGothicBook.ttf"),
41
    ("Franklin_Gothic_Book_Italic", "FranklinGothicBookItalic.ttf"),
42
    ("Franklin_Gothic_Demi_Regular", "FranklinGothicDemiRegular.ttf"),
43
    ("Franklin_Gothic_Medium_Italic", "FranklinGothicMediumItalic.ttf"),
44
    ("Franklin_Gothic_Medium_Regular", "FranklinGothicMediumRegular.ttf"),
45
]:
46
    pdfmetrics.registerFont(TTFont(font_name, BASE_DIR + "/fonts/" + font_filename))
5✔
47

48
defaultPageSize = letter
5✔
49
PAGE_HEIGHT = defaultPageSize[1]
5✔
50
PAGE_WIDTH = defaultPageSize[0]
5✔
51

52

53
def sha_hash(s: str):
5✔
54
    """Hash a given string."""
NEW
55
    return sha256(s.encode("utf-8")).hexdigest()
×
56

57

58
# Extend TableOfContents class to create ListOfFigures class
59
class ListOfFigures(TableOfContents):
5✔
60
    """Build a table of figures."""
61

62
    def notify(self, kind, stuff):
5✔
63
        """
64
        Call the notification hook to register all kinds of events.
65

66
        Here we are interested in 'Figure' events only.
67
        """
68
        if kind == "TOCFigure":
5✔
69
            self.addEntry(*stuff)
5✔
70

71

72
# Extend TableOfContents class to create ListOfTables class
73
class ListOfTables(TableOfContents):
5✔
74
    """Build a table of tables."""
75

76
    def notify(self, kind, stuff):
5✔
77
        """Call the notification hook to register all kinds of events.
78

79
        Here we are interested in 'Table' events only.
80
        """
81
        if kind == "TOCTable":
5✔
82
            self.addEntry(*stuff)
5✔
83

84

85
class MyDocTemplate(BaseDocTemplate):
5✔
86
    """Extend the BaseDocTemplate to adjust Template."""
87

88
    def __init__(self, filename, **kw):
5✔
89
        """Initialize MyDocTemplate."""
90
        self.allowSplitting = 0
5✔
91
        BaseDocTemplate.__init__(self, filename, **kw)
5✔
92
        self.pagesize = defaultPageSize
5✔
93

94
    def afterFlowable(self, flowable):
5✔
95
        """Register TOC, TOT, and TOF entries."""
96
        if flowable.__class__.__name__ == "Paragraph":
5✔
97
            text = flowable.getPlainText()
5✔
98
            style = flowable.style.name
5✔
99
            if style == "Heading1":
5✔
100
                level = 0
5✔
101
                notification = "TOCEntry"
5✔
102
            elif style == "Heading2":
5✔
103
                level = 1
5✔
104
                notification = "TOCEntry"
5✔
105
            elif style == "figure":
5✔
106
                level = 0
5✔
107
                notification = "TOCFigure"
5✔
108
            elif style == "table":
5✔
109
                level = 0
5✔
110
                notification = "TOCTable"
5✔
111
            else:
112
                return
5✔
113
            E = [level, text, self.page]
5✔
114
            # if we have a bookmark name, append that to our notify data
115
            bn = getattr(flowable, "_bookmarkName", None)
5✔
116
            if bn is not None:
5!
117
                E.append(bn)
5✔
118
            self.notify(notification, tuple(E))
5✔
119

120

121
class ConditionalSpacer(Spacer):
5✔
122
    """Create a Conditional Spacer class."""
123

124
    def wrap(self, availWidth, availHeight):
5✔
125
        """Create a spacer if there is space on the page to do so."""
126
        height = min(self.height, availHeight - 1e-8)
5✔
127
        return (availWidth, height)
5✔
128

129

130
def get_image(path, width=1 * inch):
5✔
131
    """Read in an image and scale it based on the width argument."""
132
    img = utils.ImageReader(path)
5✔
133
    iw, ih = img.getSize()
5✔
134
    aspect = ih / float(iw)
5✔
135
    return Image(path, width=width, height=(width * aspect))
5✔
136

137

138
def format_table(
5✔
139
    df, header_style, column_widths, column_style_list, remove_symbols=False
140
):
141
    """Read in a dataframe and convert it to a table and format it with a provided style list."""
142
    header_row = [
5✔
143
        [Paragraph(str(cell), header_style) for cell in row] for row in [df.columns]
144
    ]
145
    data = []
5✔
146
    for row in np.array(df).tolist():
5!
NEW
147
        current_cell = 0
×
NEW
148
        current_row = []
×
NEW
149
        for cell in row:
×
NEW
150
            if column_style_list[current_cell] is not None:
×
151
                # Remove emojis from content because the report generator can't display them
NEW
152
                cell = Paragraph(
×
153
                    demoji.replace(str(cell), ""), column_style_list[current_cell]
154
                )
155

NEW
156
            current_row.append(cell)
×
NEW
157
            current_cell += 1
×
NEW
158
        data.append(current_row)
×
159

160
    data = header_row + data
5✔
161

162
    table = Table(
5✔
163
        data,
164
        colWidths=column_widths,
165
        rowHeights=None,
166
        style=None,
167
        splitByRow=1,
168
        repeatRows=1,
169
        repeatCols=0,
170
        rowSplitRange=(2, -1),
171
        spaceBefore=None,
172
        spaceAfter=None,
173
        cornerRadii=None,
174
    )
175

176
    style = TableStyle(
5✔
177
        [
178
            ("VALIGN", (0, 0), (-1, 0), "MIDDLE"),
179
            ("ALIGN", (0, 0), (-1, -1), "CENTER"),
180
            ("VALIGN", (0, 1), (-1, -1), "MIDDLE"),
181
            ("INNERGRID", (0, 0), (-1, -1), 1, "white"),
182
            ("TEXTFONT", (0, 1), (-1, -1), "Franklin_Gothic_Book"),
183
            ("FONTSIZE", (0, 1), (-1, -1), 12),
184
            (
185
                "ROWBACKGROUNDS",
186
                (0, 1),
187
                (-1, -1),
188
                [HexColor("#FFFFFF"), HexColor("#DEEBF7")],
189
            ),
190
            ("BACKGROUND", (0, 0), (-1, 0), HexColor("#1d5288")),
191
            ("LINEBELOW", (0, -1), (-1, -1), 1.5, HexColor("#1d5288")),
192
        ]
193
    )
194
    table.setStyle(style)
5✔
195

196
    if len(df) == 0:
5!
197
        label = Paragraph(
5✔
198
            "No Data to Report",
199
            ParagraphStyle(
200
                name="centered",
201
                fontName="Franklin_Gothic_Medium_Regular",
202
                textColor=HexColor("#a7a7a6"),
203
                fontSize=16,
204
                leading=16,
205
                alignment=1,
206
                spaceAfter=10,
207
                spaceBefore=10,
208
            ),
209
        )
210
        table = KeepTogether([table, label])
5✔
211
    return table
5✔
212

213

214
def build_kpi(data, width):
5✔
215
    """Build a KPI element."""
216
    table = Table(
5✔
217
        [[data]],
218
        colWidths=[width * inch],
219
        rowHeights=60,
220
        style=None,
221
        splitByRow=1,
222
        repeatRows=0,
223
        repeatCols=0,
224
        rowSplitRange=None,
225
        spaceBefore=None,
226
        spaceAfter=None,
227
        cornerRadii=[10, 10, 10, 10],
228
    )
229

230
    style = TableStyle(
5✔
231
        [
232
            ("VALIGN", (0, 0), (-1, 0), "MIDDLE"),
233
            ("ALIGN", (0, 0), (-1, -1), "CENTER"),
234
            ("VALIGN", (0, 1), (-1, -1), "MIDDLE"),
235
            ("GRID", (0, 0), (0, 0), 1, HexColor("#003e67")),
236
            ("BACKGROUND", (0, 0), (0, 0), HexColor("#DEEBF7")),
237
        ]
238
    )
239
    table.setStyle(style)
5✔
240
    return table
5✔
241

242

243
def report_gen(data_dict, soc_med_included=False):
5✔
244
    """Generate a P&E report with data passed in the data dictionry."""
245

246
    def titlePage(canvas, doc):
5✔
247
        """Build static elements of the cover page."""
248
        canvas.saveState()
5✔
249
        canvas.drawImage(BASE_DIR + "/assets/Cover.png", 0, 0, width=None, height=None)
5✔
250
        canvas.setFont("Franklin_Gothic_Medium_Regular", 32)
5✔
251
        canvas.drawString(50, 660, "Posture & Exposure Report")
5✔
252
        canvas.restoreState()
5✔
253

254
    def summaryPage(canvas, doc):
5✔
255
        """Build static elements of the summary page."""
256
        canvas.saveState()
5✔
257
        canvas.setFont("Franklin_Gothic_Book", 13)
5✔
258
        canvas.drawImage(
5✔
259
            BASE_DIR + "/assets/summary-background.png",
260
            0,
261
            0,
262
            width=PAGE_WIDTH,
263
            height=PAGE_HEIGHT,
264
        )
265
        canvas.setFillColor(HexColor("#1d5288"))
5✔
266
        canvas.setStrokeColor("#1d5288")
5✔
267
        canvas.rect(inch, 210, 3.5 * inch, 5.7 * inch, fill=1)
5✔
268
        canvas.restoreState()
5✔
269
        summary_frame = Frame(
5✔
270
            1.1 * inch, 224, 3.3 * inch, 5.5 * inch, id=None, showBoundary=0
271
        )
272
        summary_1_style = ParagraphStyle(
5✔
273
            "summary_1_style",
274
            fontSize=12,
275
            alignment=0,
276
            textColor="white",
277
            fontName="Franklin_Gothic_Book",
278
        )
279
        summary_1 = Paragraph(
5✔
280
            """
281
        <font face="Franklin_Gothic_Medium_Regular">Credential Publication & Abuse:</font><br/>
282
        User credentials, often including passwords, are stolen or exposed via data breaches. They are then listed for sale on forums and the dark web, which provides attackers easy access to a stakeholders' network.
283
        <br/><br/><br/><br/>
284
        <font face="Franklin_Gothic_Medium_Regular">Suspected Domain Masquerading Attempt:</font><br/>
285
        Registered domain names that are similar to legitimate domains which attempt to trick users into navigating to illegitimate domains.
286
        <br/><br/><br/><br/><br/><br/>
287
        <font face="Franklin_Gothic_Medium_Regular">Insecure Devices & Vulnerabilities:</font><br/>
288
        Open ports, risky protocols, insecure products, and externally observable vulnerabilities are potential targets for exploit.
289
        <br/><br/><br/><br/><br/>
290
        <font face="Franklin_Gothic_Medium_Regular">Dark Web Activity:</font><br/>
291
        Heightened public attention can indicate increased targeting and attack coordination, especially when attention is found on the dark web.
292
        """,
293
            style=summary_1_style,
294
        )
295
        summary_frame.addFromList([summary_1], canvas)
5✔
296

297
        summary_frame_2 = Frame(
5✔
298
            5.1 * inch, 552, 2.4 * inch, 0.7 * inch, id=None, showBoundary=0
299
        )
300
        summary_2 = Paragraph(
5✔
301
            str(data_dict["creds"])
302
            + """<br/> <font face="Franklin_Gothic_Book" size='10'>Total Credential Publications</font>""",
303
            style=kpi,
304
        )
305
        summary_frame_2.addFromList([summary_2], canvas)
5✔
306

307
        summary_frame_3 = Frame(
5✔
308
            5.1 * inch, 444, 2.4 * inch, 0.7 * inch, id=None, showBoundary=0
309
        )
310
        summary_3 = Paragraph(
5✔
311
            str(data_dict["suspectedDomains"])
312
            + """<br/> <font face="Franklin_Gothic_Book" size='10'>Suspected Domain Masquerading</font>""",
313
            style=kpi,
314
        )
315
        summary_frame_3.addFromList([summary_3], canvas)
5✔
316

317
        summary_frame_4 = Frame(
5✔
318
            5.1 * inch, 337, 2.4 * inch, 0.7 * inch, id=None, showBoundary=0
319
        )
320
        summary_4 = Paragraph(
5✔
321
            str(data_dict["verifVulns"])
322
            + """<br/> <font face="Franklin_Gothic_Book" size='10'>Shodan Verified Vulnerabilities Found</font>""",
323
            style=kpi,
324
        )
325
        summary_frame_4.addFromList([summary_4], canvas)
5✔
326

327
        summary_frame_5 = Frame(
5✔
328
            5.1 * inch, 230, 2.4 * inch, 0.7 * inch, id=None, showBoundary=0
329
        )
330
        summary_5 = Paragraph(
5✔
331
            str(data_dict["darkWeb"])
332
            + """<br/> <font face="Franklin_Gothic_Book" size='10'>Dark Web Alerts</font>""",
333
            style=kpi,
334
        )
335
        summary_frame_5.addFromList([summary_5], canvas)
5✔
336

337
        json_title_frame = Frame(
5✔
338
            3.85 * inch, 175, 1.5 * inch, 0.5 * inch, id=None, showBoundary=0
339
        )
340
        json_title = Paragraph(
5✔
341
            "JSON&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;EXCEL",
342
            style=json_excel,
343
        )
344
        json_title_frame.addFromList([json_title], canvas)
5✔
345

346
        canvas.setStrokeColor("#a7a7a6")
5✔
347
        canvas.setFillColor("#a7a7a6")
5✔
348
        canvas.drawInlineImage(
5✔
349
            BASE_DIR + "/assets/cisa.png", 45, 705, width=65, height=65
350
        )
351
        canvas.drawString(130, 745, "Posture and Exposure Report")
5✔
352
        canvas.drawString(130, 725, "Reporting Period: " + data_dict["dateRange"])
5✔
353
        canvas.line(130, 710, PAGE_WIDTH - inch, 710)
5✔
354
        canvas.drawRightString(
5✔
355
            PAGE_WIDTH - inch, 0.75 * inch, "P&E Report | Page %d" % (doc.page)
356
        )
357
        canvas.drawString(inch, 0.75 * inch, data_dict["endDate"])
5✔
358
        canvas.setFont("Franklin_Gothic_Medium_Regular", 12)
5✔
359
        canvas.setFillColor("#FFC000")
5✔
360
        canvas.drawString(6.4 * inch, 745, "TLP: AMBER")
5✔
361

362
    def contentPage(canvas, doc):
5✔
363
        """Build the header and footer content for the rest of the pages in the report."""
364
        canvas.saveState()
5✔
365
        canvas.setFont("Franklin_Gothic_Book", 12)
5✔
366
        canvas.setStrokeColor("#a7a7a6")
5✔
367
        canvas.setFillColor("#a7a7a6")
5✔
368
        canvas.drawImage(BASE_DIR + "/assets/cisa.png", 45, 705, width=65, height=65)
5✔
369
        canvas.drawString(130, 745, "Posture and Exposure Report")
5✔
370
        canvas.drawString(130, 725, "Reporting Period: " + data_dict["dateRange"])
5✔
371
        canvas.line(130, 710, PAGE_WIDTH - inch, 710)
5✔
372
        canvas.drawRightString(
5✔
373
            PAGE_WIDTH - inch, 0.75 * inch, "P&E Report | Page %d" % (doc.page)
374
        )
375
        canvas.drawString(inch, 0.75 * inch, data_dict["endDate"])
5✔
376
        canvas.setFont("Franklin_Gothic_Medium_Regular", 12)
5✔
377
        canvas.setFillColor("#FFC000")
5✔
378
        canvas.drawString(6.4 * inch, 745, "TLP: AMBER")
5✔
379
        canvas.restoreState()
5✔
380

381
    def doHeading(text, sty):
5✔
382
        """Add a bookmark to heading element to allow linking from the table of contents."""
383
        # create bookmarkname
384
        bn = sha256((text + sty.name).encode("utf8")).hexdigest()
5✔
385
        # modify paragraph text to include an anchor point with name bn
386
        h = Paragraph(text + '<a name="%s"/>' % bn, sty)
5✔
387
        # store the bookmark name on the flowable so afterFlowable can see this
388
        h._bookmarkName = bn
5✔
389
        return h
5✔
390

391
    # ***Document Structures***#
392
    """Build frames for different page structures."""
1✔
393
    doc = MyDocTemplate(data_dict["filename"])
5✔
394
    title_frame = Frame(45, 390, 530, 250, id=None, showBoundary=0)
5✔
395
    frameT = Frame(
5✔
396
        doc.leftMargin,
397
        doc.bottomMargin,
398
        PAGE_WIDTH - (2 * inch),
399
        PAGE_HEIGHT - (2.4 * inch),
400
        id="normal",
401
        showBoundary=0,
402
    )
403
    doc.addPageTemplates(
5✔
404
        [
405
            PageTemplate(id="TitlePage", frames=title_frame, onPage=titlePage),
406
            PageTemplate(id="SummaryPage", frames=frameT, onPage=summaryPage),
407
            PageTemplate(id="ContentPage", frames=frameT, onPage=contentPage),
408
        ]
409
    )
410
    Story = []
5✔
411
    """Build table of contents."""
1✔
412
    toc = TableOfContents()
5✔
413
    tof = ListOfFigures()
5✔
414
    tot = ListOfTables()
5✔
415

416
    """Create font and formatting styles."""
1✔
417
    PS = ParagraphStyle
5✔
418

419
    centered = PS(
5✔
420
        name="centered",
421
        fontName="Franklin_Gothic_Medium_Regular",
422
        fontSize=20,
423
        leading=16,
424
        alignment=1,
425
        spaceAfter=10,
426
        spaceBefore=10,
427
    )
428

429
    indented = PS(
5✔
430
        name="indented",
431
        fontName="Franklin_Gothic_Book",
432
        fontSize=12,
433
        leading=14,
434
        leftIndent=30,
435
        spaceAfter=20,
436
    )
437

438
    h1 = PS(
5✔
439
        fontName="Franklin_Gothic_Medium_Regular",
440
        name="Heading1",
441
        fontSize=16,
442
        leading=18,
443
        textColor=HexColor("#003e67"),
444
    )
445

446
    h2 = PS(
5✔
447
        name="Heading2",
448
        fontName="Franklin_Gothic_Medium_Regular",
449
        fontSize=14,
450
        leading=10,
451
        textColor=HexColor("#003e67"),
452
        spaceAfter=12,
453
    )
454

455
    h3 = PS(
5✔
456
        name="Heading3",
457
        fontName="Franklin_Gothic_Medium_Regular",
458
        fontSize=14,
459
        leading=10,
460
        textColor=HexColor("#003e67"),
461
        spaceAfter=10,
462
    )
463

464
    body = PS(
5✔
465
        name="body",
466
        leading=14,
467
        fontName="Franklin_Gothic_Book",
468
        fontSize=12,
469
    )
470

471
    kpi = PS(
5✔
472
        name="kpi",
473
        fontName="Franklin_Gothic_Medium_Regular",
474
        fontSize=14,
475
        leading=16,
476
        alignment=1,
477
        spaceAfter=20,
478
    )
479

480
    json_excel = PS(
5✔
481
        name="json_excel",
482
        fontName="Franklin_Gothic_Medium_Regular",
483
        fontSize=10,
484
        alignment=1,
485
    )
486

487
    figure = PS(
5✔
488
        name="figure",
489
        fontName="Franklin_Gothic_Medium_Regular",
490
        fontSize=12,
491
        leading=16,
492
        alignment=1,
493
    )
494

495
    table = PS(
5✔
496
        name="table",
497
        fontName="Franklin_Gothic_Medium_Regular",
498
        fontSize=12,
499
        leading=16,
500
        alignment=1,
501
        spaceAfter=12,
502
    )
503

504
    table_header = PS(
5✔
505
        name="table_header",
506
        fontName="Franklin_Gothic_Medium_Regular",
507
        fontSize=12,
508
        leading=16,
509
        alignment=1,
510
        spaceAfter=12,
511
        textColor=HexColor("#FFFFFF"),
512
    )
513

514
    title_data = PS(
5✔
515
        fontName="Franklin_Gothic_Medium_Regular", name="Title", fontSize=18, leading=20
516
    )
517

518
    """Stream all the dynamic content to the report."""
1✔
519

520
    # *************************#
521
    # Create repeated elements
522
    point12_spacer = ConditionalSpacer(1, 12)
5✔
523
    horizontal_line = HRFlowable(
5✔
524
        width="100%",
525
        thickness=1.5,
526
        lineCap="round",
527
        color=HexColor("#003e67"),
528
        spaceBefore=0,
529
        spaceAfter=1,
530
        hAlign="LEFT",
531
        vAlign="TOP",
532
        dash=None,
533
    )
534
    # ***Title Page***#
535
    Story.append(Paragraph("Prepared for: " + data_dict["department"], title_data))
5✔
536
    Story.append(point12_spacer)
5✔
537
    Story.append(Paragraph("Reporting Period: " + data_dict["dateRange"], title_data))
5✔
538
    Story.append(NextPageTemplate("ContentPage"))
5✔
539
    Story.append(PageBreak())
5✔
540

541
    # ***Table of Contents***#
542
    Story.append(Paragraph("<b>Table of Contents</b>", centered))
5✔
543
    # Set styles for levels in Table of contents
544
    toc_styles = [
5✔
545
        PS(
546
            fontName="Franklin_Gothic_Medium_Regular",
547
            fontSize=14,
548
            name="TOCHeading1",
549
            leftIndent=20,
550
            firstLineIndent=-20,
551
            spaceBefore=1,
552
            leading=14,
553
        ),
554
        PS(
555
            fontSize=12,
556
            name="TOCHeading2",
557
            leftIndent=40,
558
            firstLineIndent=-20,
559
            spaceBefore=0,
560
            leading=12,
561
        ),
562
        PS(
563
            fontSize=10,
564
            name="TOCHeading3",
565
            leftIndent=60,
566
            firstLineIndent=-20,
567
            spaceBefore=0,
568
            leading=12,
569
        ),
570
        PS(
571
            fontSize=10,
572
            name="TOCHeading4",
573
            leftIndent=100,
574
            firstLineIndent=-20,
575
            spaceBefore=0,
576
            leading=12,
577
        ),
578
    ]
579
    toc.levelStyles = toc_styles
5✔
580
    Story.append(toc)
5✔
581
    Story.append(PageBreak())
5✔
582

583
    # ***Table of Figures and Table of Contents***#
584
    tot.levelStyles = toc_styles
5✔
585
    tof.levelStyles = toc_styles
5✔
586
    Story.append(Paragraph("<b>Table of Figures</b>", centered))
5✔
587
    Story.append(tof)
5✔
588
    Story.append(Paragraph("<b>Table of Tables</b>", centered))
5✔
589
    Story.append(tot)
5✔
590
    Story.append(PageBreak())
5✔
591

592
    # ***Content Pages***#
593
    # ***Start Introduction Page***#
594
    Story.append(doHeading("1. Introduction", h1))
5✔
595
    Story.append(horizontal_line)
5✔
596
    Story.append(point12_spacer)
5✔
597
    Story.append(doHeading("1.1 Overview", h2))
5✔
598
    Story.append(
5✔
599
        Paragraph(
600
            """Posture and Exposure (P&E) offers stakeholders an opportunity to view their organizational
601
                risk from the viewpoint of the adversary. We utilize passive reconnaissance services,
602
                dark web analysis, and open-source tools to identify spoofing in order to generate a risk
603
                    profile report that is delivered on a regular basis.<br/><br/>
604
                As a customer of P&E you are receiving our regularly scheduled report which contains a
605
                summary of the activity we have been tracking on your behalf for the following services:
606
                <br/><br/>""",
607
            body,
608
        )
609
    )
610

611
    Story.append(
5✔
612
        ListFlowable(
613
            [
614
                ListItem(
615
                    Paragraph("Domain Masquerading and Monitoring", body),
616
                    leftIndent=35,
617
                    value="bulletchar",
618
                ),
619
                ListItem(
620
                    Paragraph("Vulnerabilities & Malware Associations", body),
621
                    leftIndent=35,
622
                    value="bulletchar",
623
                ),
624
                ListItem(
625
                    Paragraph("Dark Web Monitoring", body),
626
                    leftIndent=35,
627
                    value="bulletchar",
628
                ),
629
                ListItem(
630
                    Paragraph("Hidden Assets and Risky Services", body),
631
                    leftIndent=35,
632
                    value="bulletchar",
633
                ),
634
            ],
635
            bulletType="bullet",
636
            start="bulletchar",
637
            leftIndent=10,
638
        )
639
    )
640

641
    Story.append(
5✔
642
        Paragraph(
643
            """<br/>It is important to note that these findings have not been verified; everything is
644
                            gathered via passive analysis of publicly available sources. As such there may be false
645
                            positive findings; however, these findings should be treated as information that your
646
                            organization is leaking out to the internet for adversaries to notice.<br/><br/>""",
647
            body,
648
        )
649
    )
650

651
    Story.append(doHeading("1.2 How to use this report", h2))
5✔
652
    Story.append(
5✔
653
        Paragraph(
654
            """While it is not our intent to prescribe to you a particular process for remediating
655
                            vulnerabilities, we hope you will use this report to strengthen your security posture.
656
                            Here is a basic flow:<br/><br/>""",
657
            body,
658
        )
659
    )
660
    Story.append(
5✔
661
        ListFlowable(
662
            [
663
                ListItem(
664
                    Paragraph(
665
                        """Review the Summary of Findings on page 5. This section gives a quick overview of key
666
                            results including the number of credential exposures, domain masquerading alerts, Shodan
667
                            verified vulnerabilites, and dark web alerts.""",
668
                        body,
669
                    ),
670
                    leftIndent=35,
671
                ),
672
                ListItem(
673
                    Paragraph(
674
                        """Dive deeper into those key findings by investigating the detailed results starting on
675
                            page 6.""",
676
                        body,
677
                    ),
678
                    leftIndent=35,
679
                ),
680
                ListItem(
681
                    Paragraph(
682
                        """Want to see our raw data? Navigate to page 5 where you can open the embedded Excel
683
                            files. If you are having trouble opening these files, make sure to use Adobe Acrobat.""",
684
                        body,
685
                    ),
686
                    leftIndent=35,
687
                ),
688
                ListItem(
689
                    Paragraph(
690
                        """More questions? Please refer to the Frequently Asked Questions found on page 19. Please
691
                            feel free to contact us at vulnerability@cisa.gov with any further questions or concerns.<br/><br/>""",
692
                        body,
693
                    ),
694
                    leftIndent=35,
695
                ),
696
            ],
697
            bulletType="1",
698
            bulletFormat="%s.",
699
            leftIndent=10,
700
            bulletFontSize=12,
701
        )
702
    )
703

704
    Story.append(doHeading("1.3 Contact Information", h2))
5✔
705
    Story.append(
5✔
706
        Paragraph("Posture and Exposure Team Email: vulnerability@cisa.dhs.gov", body)
707
    )
708

709
    Story.append(NextPageTemplate("SummaryPage"))
5✔
710
    Story.append(PageBreak())
5✔
711

712
    # ***Start Generating Summary Page***#
713
    Story.append(doHeading("2. Summary of Findings", h1))
5✔
714
    Story.append(horizontal_line)
5✔
715
    Story.append(point12_spacer)
5✔
716
    Story.append(doHeading("2.1 Summary of Tracked Data", h2))
5✔
717
    Story.append(Spacer(1, 425))
5✔
718
    Story.append(doHeading("2.2 Raw Data Links", h2))
5✔
719
    Story.append(
5✔
720
        Paragraph(
721
            "Exposed Credentials<br/><br/>Domain Masquerading and Monitoring<br/><br/>Vulnerabilities and Malware Associations<br/><br/>Dark Web Activity",
722
            body,
723
        )
724
    )
725

726
    Story.append(NextPageTemplate("ContentPage"))
5✔
727
    Story.append(PageBreak())
5✔
728

729
    # ***Start Generating Creds Page***#
730
    Story.append(doHeading("3. Detailed Results", h1))
5✔
731
    Story.append(horizontal_line)
5✔
732
    Story.append(point12_spacer)
5✔
733
    Story.append(doHeading("3.1 Credential Publication and Abuse", h2))
5✔
734
    Story.append(
5✔
735
        Paragraph(
736
            """Credential leakage occurs when user credentials, often including passwords, are stolen via phishing
737
        campaigns, network compromise, or database misconfigurations leading to public exposure. This leaked data is
738
        then listed for sale on numerous forums and sites on the dark web which provides attackers easy access to a
739
        stakeholder's networks. Detailed results are presented below.
740
        """,
741
            body,
742
        )
743
    )
744

745
    # Build row of kpi cells
746
    row = [
5✔
747
        build_kpi(
748
            Paragraph(
749
                str(data_dict["breach"])
750
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Distinct Breaches</font>""",
751
                style=kpi,
752
            ),
753
            2,
754
        ),
755
        build_kpi(
756
            Paragraph(
757
                str(data_dict["creds"])
758
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Credentials Exposed</font>""",
759
                style=kpi,
760
            ),
761
            2,
762
        ),
763
        build_kpi(
764
            Paragraph(
765
                str(data_dict["pw_creds"])
766
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Credentials with Password</font>""",
767
                style=kpi,
768
            ),
769
            2,
770
        ),
771
    ]
772
    Story.append(
5✔
773
        BalancedColumns(
774
            row,  # the flowables we are balancing
775
            nCols=3,  # the number of columns
776
            needed=55,  # the minimum space needed by the flowable
777
            spaceBefore=0,
778
            spaceAfter=12,
779
            showBoundary=False,  # optional boundary showing
780
            leftPadding=4,  # these override the created frame
781
            rightPadding=0,  # paddings if specified else the
782
            topPadding=None,  # default frame paddings
783
            bottomPadding=None,  # are used
784
            innerPadding=8,  # the gap between frames if specified else
785
            # use max(leftPadding,rightPadding)
786
            name="creds_kpis",  # for identification purposes when stuff goes awry
787
            endSlack=0.1,  # height disparity allowance ie 10% of available height
788
        )
789
    )
790

791
    Story.append(
5✔
792
        Paragraph(
793
            """
794
            <font face="Franklin_Gothic_Medium_Regular">Figure 1</font> shows the credentials exposed during each week of the reporting period, including those with no
795
            passwords as well as those with passwords included.
796
        """,
797
            body,
798
        )
799
    )
800
    Story.append(point12_spacer)
5✔
801
    Story.append(
5✔
802
        KeepTogether(
803
            [
804
                doHeading(
805
                    """
806
                        Figure 1. Credentials Exposed.
807
                    """,
808
                    figure,
809
                ),
810
                get_image(BASE_DIR + "/assets/inc_date_df.png", width=6.5 * inch),
811
            ]
812
        )
813
    )
814

815
    Story.append(PageBreak())
5✔
816
    Story.append(
5✔
817
        Paragraph(
818
            """
819
            <font face="Franklin_Gothic_Medium_Regular">Table 1</font>  provides breach details. Breach descriptions can be found in Appendix A.
820
        """,
821
            body,
822
        )
823
    )
824
    Story.append(point12_spacer)
5✔
825
    Story.append(
5✔
826
        doHeading(
827
            """
828
                    Table 1. Breach Details.
829
                """,
830
            table,
831
        )
832
    )
833

834
    # add link to appendix to breach names
835
    data_dict["breach_table"]["Breach Name"] = (
5✔
836
        '<link href="#'
837
        + data_dict["breach_table"]["Breach Name"].apply(sha_hash)
838
        + '" color="#003e67">'
839
        + data_dict["breach_table"]["Breach Name"].astype(str)
840
        + "</link>"
841
    )
842
    Story.append(
5✔
843
        format_table(
844
            data_dict["breach_table"],
845
            table_header,
846
            [2.5 * inch, inch, inch, inch, inch],
847
            [body, None, None, None, None],
848
        )
849
    )
850

851
    Story.append(point12_spacer)
5✔
852
    Story.append(PageBreak())
5✔
853

854
    # ***Start Generating Domain Masquerading Page***#
855
    Story.append(
5✔
856
        KeepTogether(
857
            [
858
                doHeading("3.2 Domain Alerts and Suspected Masquerading", h2),
859
                Paragraph(
860
                    """Spoofed or typo-squatting domains can be used to host fake web pages for malicious purposes,
861
            such as imitating landing pages for spear phishing campaigns. Below are alerts of domains that appear
862
            to mimic a stakeholder's actual domain.
863
            """,
864
                    body,
865
                ),
866
                point12_spacer,
867
            ]
868
        )
869
    )
870

871
    row = [
5✔
872
        build_kpi(
873
            Paragraph(
874
                str(data_dict["domain_alerts"])
875
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Domain Alert(s)</font>""",
876
                style=kpi,
877
            ),
878
            2,
879
        ),
880
        build_kpi(
881
            Paragraph(
882
                str(data_dict["suspectedDomains"])
883
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Suspected Domain(s)</font>""",
884
                style=kpi,
885
            ),
886
            2,
887
        ),
888
    ]
889

890
    Story.append(
5✔
891
        BalancedColumns(
892
            row,  # the flowables we are balancing
893
            nCols=2,  # the number of columns
894
            needed=55,  # the minimum space needed by the flowable
895
            spaceBefore=0,
896
            spaceAfter=12,
897
            showBoundary=False,  # optional boundary showing
898
            leftPadding=65,  # these override the created frame
899
            rightPadding=0,  # paddings if specified else the
900
            topPadding=None,  # default frame paddings
901
            bottomPadding=None,  # are used
902
            innerPadding=35,  # the gap between frames if specified else
903
            # use max(leftPadding,rightPadding)
904
            name="domain_masq_kpis",  # for identification purposes when stuff goes awry
905
            endSlack=0.1,  # height disparity allowance ie 10% of available height
906
        )
907
    )
908

909
    Story.append(Paragraph("3.2.1 Domain Monitoring Alerts", h3))
5✔
910
    Story.append(
5✔
911
        Paragraph(
912
            """
913
            <font face="Franklin_Gothic_Medium_Regular">Table 2</font> shows alerts of newly registered or updated
914
            domains that appear to mimic a stakeholder's actual domain.
915
        """,
916
            body,
917
        )
918
    )
919
    Story.append(point12_spacer)
5✔
920
    Story.append(
5✔
921
        doHeading(
922
            """
923
                    Table 2. Domain Monitoring Alerts Results.
924
                """,
925
            table,
926
        )
927
    )
928
    Story.append(
5✔
929
        format_table(
930
            data_dict["domain_alerts_table"],
931
            table_header,
932
            [5.5 * inch, 1 * inch],
933
            [body, None],
934
        )
935
    )
936

937
    Story.append(point12_spacer)
5✔
938
    Story.append(
5✔
939
        KeepTogether(
940
            [
941
                Paragraph("3.2.2 Suspected Domain Masquerading", h3),
942
                Paragraph(
943
                    """
944
                    <font face="Franklin_Gothic_Medium_Regular">Table 3</font> shows registered or updated domains that were
945
                    flagged by a blocklist service.
946
                """,
947
                    body,
948
                ),
949
                point12_spacer,
950
                doHeading(
951
                    """
952
                    Table 3. Suspected Domain Masquerading Results.
953
                """,
954
                    table,
955
                ),
956
            ]
957
        )
958
    )
959

960
    Story.append(
5✔
961
        format_table(
962
            data_dict["domain_table"],
963
            table_header,
964
            [1.5 * inch, 1.5 * inch, 3.5 * inch / 3, 3.5 * inch / 3, 3.5 * inch / 3],
965
            [body, body, body, body, body],
966
        )
967
    )
968
    Story.append(point12_spacer)
5✔
969

970
    Story.append(PageBreak())
5✔
971

972
    # Start generating Vulnerabilities page
973
    Story.append(
5✔
974
        KeepTogether(
975
            [
976
                doHeading("3.3 Insecure Devices & Suspected Vulnerabilities", h2),
977
                Paragraph(
978
                    """This category includes insecure ports, protocols, and services; Shodan-verified vulnerabilities;
979
                and suspected vulnerabilities. Detailed results are presented below and discussed in the sections that follow.
980
                """,
981
                    body,
982
                ),
983
                point12_spacer,
984
            ]
985
        )
986
    )
987
    row = [
5✔
988
        build_kpi(
989
            Paragraph(
990
                str(data_dict["riskyPorts"])
991
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Total Open Ports with <br/>Insecure Protocols</font>""",
992
                style=kpi,
993
            ),
994
            2,
995
        ),
996
        build_kpi(
997
            Paragraph(
998
                str(data_dict["verifVulns"])
999
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Total Shodan-Verified Vulnerabilities</font>""",
1000
                style=kpi,
1001
            ),
1002
            2,
1003
        ),
1004
        build_kpi(
1005
            Paragraph(
1006
                str(data_dict["unverifVulns"])
1007
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Assets with Suspected Vulnerabilities</font>""",
1008
                style=kpi,
1009
            ),
1010
            2,
1011
        ),
1012
    ]
1013
    Story.append(
5✔
1014
        BalancedColumns(
1015
            row,  # the flowables we are balancing
1016
            nCols=3,  # the number of columns
1017
            needed=55,  # the minimum space needed by the flowable
1018
            spaceBefore=0,
1019
            spaceAfter=12,
1020
            showBoundary=False,  # optional boundary showing
1021
            leftPadding=4,  # these override the created frame
1022
            rightPadding=0,  # paddings if specified else the
1023
            topPadding=None,  # default frame paddings
1024
            bottomPadding=None,  # are used
1025
            innerPadding=8,  # the gap between frames if specified else
1026
            name="vulns_kpis",  # for identification purposes when stuff goes awry
1027
            endSlack=0.1,  # height disparity allowance ie 10% of available height
1028
        )
1029
    )
1030

1031
    Story.append(Paragraph("3.3.1 Insecure Ports, Protocols, and Services", h3))
5✔
1032
    Story.append(
5✔
1033
        Paragraph(
1034
            """
1035
            Insecure protocols are those protocols which lack proper encryption allowing threat actors to access
1036
            data that is being transmitted and even to potentially, to control systems.
1037
            <font face="Franklin_Gothic_Medium_Regular">Figure 2</font> and
1038
            <font face="Franklin_Gothic_Medium_Regular">Table 4</font> provide detailed information for the Remote
1039
            Desktop Protocol (RDP), Server Message Block (SMB) protocol, and the Telnet application protocol.
1040
        """,
1041
            body,
1042
        )
1043
    )
1044
    Story.append(point12_spacer)
5✔
1045
    Story.append(
5✔
1046
        KeepTogether(
1047
            [
1048
                doHeading(
1049
                    """
1050
                        Figure 2. Insecure Protocols.
1051
                    """,
1052
                    figure,
1053
                ),
1054
                get_image(BASE_DIR + "/assets/pro_count.png", width=6.5 * inch),
1055
            ]
1056
        )
1057
    )
1058
    Story.append(
5✔
1059
        doHeading(
1060
            """
1061
                Table 4. Insecure Protocols.
1062
            """,
1063
            table,
1064
        )
1065
    )
1066
    Story.append(
5✔
1067
        format_table(
1068
            data_dict["risky_assets"],
1069
            table_header,
1070
            [1.5 * inch, 3.5 * inch, 1.5 * inch],
1071
            [None, body, None],
1072
        )
1073
    )
1074

1075
    Story.append(point12_spacer)
5✔
1076
    Story.append(
5✔
1077
        KeepTogether(
1078
            [
1079
                Paragraph("3.3.2 Shodan-Verified Vulnerabilities", h3),
1080
                Paragraph(
1081
                    """
1082
                    Verified vulnerabilities, shown in <font face="Franklin_Gothic_Medium_Regular">Table 5</font>, are those that are flagged by P&E vendors that have gone
1083
                    through extra checks to validate the finding. Refer to Appendix A for summary data.
1084
                """,
1085
                    body,
1086
                ),
1087
                doHeading(
1088
                    """
1089
                    Table 5. Shodan-Verified Vulnerabilities.
1090
                """,
1091
                    table,
1092
                ),
1093
            ]
1094
        )
1095
    )
1096
    # add link to appendix for CVE string
1097
    data_dict["verif_vulns"]["CVE"] = (
5✔
1098
        '<link href="#'
1099
        + data_dict["verif_vulns"]["CVE"].str.replace("-", "_")
1100
        + '" color="#003e67">'
1101
        + data_dict["verif_vulns"]["CVE"].astype(str)
1102
        + "</link>"
1103
    )
1104

1105
    Story.append(
5✔
1106
        format_table(
1107
            data_dict["verif_vulns"],
1108
            table_header,
1109
            [6.5 * inch / 3, 6.5 * inch / 3, 6.5 * inch / 3],
1110
            [body, None, None],
1111
        )
1112
    )
1113

1114
    Story.append(point12_spacer)
5✔
1115

1116
    Story.append(
5✔
1117
        KeepTogether(
1118
            [
1119
                Paragraph("3.3.3 Suspected Vulnerabilities", h3),
1120
                Paragraph(
1121
                    """
1122
                        Suspected vulnerabilities are determined by the software and version an asset is running and can be used
1123
                        to understand what vulnerabilities an asset may be exposed to.
1124
                        <font face="Franklin_Gothic_Medium_Regular">Figure 3</font> identifies suspected vulnerabilities.
1125
                    """,
1126
                    body,
1127
                ),
1128
                point12_spacer,
1129
                doHeading(
1130
                    """
1131
                        Figure 3. Suspected Vulnerabilities.
1132
                    """,
1133
                    figure,
1134
                ),
1135
                get_image(
1136
                    BASE_DIR + "/assets/unverif_vuln_count.png", width=6.5 * inch
1137
                ),
1138
            ]
1139
        )
1140
    )
1141
    Story.append(PageBreak())
5✔
1142

1143
    # Start generating Dark Web page
1144
    Story.append(KeepTogether([doHeading("3.4 Dark Web Activity", h2), Spacer(1, 6)]))
5✔
1145

1146
    row = [
5✔
1147
        build_kpi(
1148
            Paragraph(
1149
                str(data_dict["mentions_count"])
1150
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Dark Web Mentions</font>""",
1151
                style=kpi,
1152
            ),
1153
            2,
1154
        ),
1155
        build_kpi(
1156
            Paragraph(
1157
                str(data_dict["darkWeb"])
1158
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Dark Web Alerts</font>""",
1159
                style=kpi,
1160
            ),
1161
            2,
1162
        ),
1163
    ]
1164

1165
    Story.append(
5✔
1166
        BalancedColumns(
1167
            row,  # the flowables we are balancing
1168
            nCols=2,  # the number of columns
1169
            needed=55,  # the minimum space needed by the flowable
1170
            spaceBefore=0,
1171
            spaceAfter=12,
1172
            showBoundary=False,  # optional boundary showing
1173
            leftPadding=65,  # these override the created frame
1174
            rightPadding=0,  # paddings if specified else the
1175
            topPadding=None,  # default frame paddings
1176
            bottomPadding=None,  # are used
1177
            innerPadding=35,  # the gap between frames if specified else
1178
            name="dark_web_kpis",  # for identification purposes when stuff goes awry
1179
            endSlack=0.1,  # height disparity allowance ie 10% of available height
1180
        )
1181
    )
1182

1183
    Story.append(
5✔
1184
        Paragraph(
1185
            """Stakeholders and vulnerabilities are often discussed in various ways on the Dark Web. P&E monitors this
1186
                activity, as well as the source (forums, websites, tutorials), and threat actors involved. A spike in activity can
1187
                indicate a greater likelihood of an attack, vulnerability, or data leakage. This information along with a list of the
1188
                most active CVEs on the Dark Web may assist in prioritizing remediation activities.""",
1189
            style=body,
1190
        )
1191
    )
1192

1193
    Story.append(point12_spacer)
5✔
1194

1195
    Story.append(Paragraph("3.4.1 Dark Web Mentions", h3))
5✔
1196
    Story.append(
5✔
1197
        Paragraph(
1198
            """
1199
            <font face="Franklin_Gothic_Medium_Regular">Figure 4</font> provides details on the number of mentions on the
1200
            dark web during the reporting period.
1201
        """,
1202
            body,
1203
        )
1204
    )
1205
    Story.append(point12_spacer)
5✔
1206
    Story.append(
5✔
1207
        KeepTogether(
1208
            [
1209
                doHeading(
1210
                    """
1211
                        Figure 4. Dark Web Mentions.
1212
                    """,
1213
                    figure,
1214
                ),
1215
                get_image(BASE_DIR + "/assets/web_only_df_2.png", width=6.5 * inch),
1216
            ]
1217
        )
1218
    )
1219
    sub_section = 2
5✔
1220
    table_num = 6
5✔
1221
    if soc_med_included:
5!
1222
        Story.append(
5✔
1223
            KeepTogether(
1224
                [
1225
                    Paragraph("3.4.2 Most Active Social Media Posts", h3),
1226
                    Paragraph(
1227
                        """
1228
                        This result includes a list of the most active social media posts associated with a stakeholder, and tallies
1229
                        the count of “post” or “reply” actions on sites such as Telegram, Twitter, and Github.
1230
                        <font face="Franklin_Gothic_Medium_Regular">Table 6</font> identifies the social media comments count
1231
                        by organization.
1232
                    """,
1233
                        body,
1234
                    ),
1235
                    point12_spacer,
1236
                    doHeading(
1237
                        """
1238
                        Table 6. Social Media Comments Count.
1239
                    """,
1240
                        table,
1241
                    ),
1242
                ]
1243
            )
1244
        )
1245

1246
        Story.append(
5✔
1247
            format_table(
1248
                data_dict["social_med_act"],
1249
                table_header,
1250
                [5 * inch, 1.5 * inch],
1251
                [
1252
                    body,
1253
                    None,
1254
                ],
1255
            )
1256
        )
1257

1258
        Story.append(point12_spacer)
5✔
1259
        sub_section = 3
5✔
1260
        table_num = 7
5✔
1261

1262
    Story.append(
5✔
1263
        KeepTogether(
1264
            [
1265
                Paragraph(
1266
                    "3.4." + str(sub_section) + " Most Active Dark Web Posts", h3
1267
                ),
1268
                Paragraph(
1269
                    """
1270
                    This result includes a list of the most active posts associated with a stakeholder found on the dark web,
1271
                    and includes forum sites and invite-only marketplaces. <font face="Franklin_Gothic_Medium_Regular">Table """
1272
                    + str(table_num)
1273
                    + """</font>
1274
                    identifies the dark web comments count by organization.
1275
                """,
1276
                    body,
1277
                ),
1278
                point12_spacer,
1279
                doHeading(
1280
                    "Table " + str(table_num) + ". Dark Web Comments Count.", table
1281
                ),
1282
            ]
1283
        )
1284
    )
1285
    sub_section += 1
5✔
1286
    table_num += 1
5✔
1287
    Story.append(
5✔
1288
        format_table(
1289
            data_dict["dark_web_act"],
1290
            table_header,
1291
            [5 * inch, 1.5 * inch],
1292
            [
1293
                body,
1294
                None,
1295
            ],
1296
        )
1297
    )
1298

1299
    Story.append(point12_spacer)
5✔
1300

1301
    Story.append(
5✔
1302
        KeepTogether(
1303
            [
1304
                Paragraph("3.4." + str(sub_section) + " Asset Alerts", h3),
1305
                Paragraph(
1306
                    """
1307
                    <font face="Franklin_Gothic_Medium_Regular">Table """
1308
                    + str(table_num)
1309
                    + """</font> includes discussions involving stakeholder
1310
                    assets such as domain names and IPs.
1311
                """,
1312
                    body,
1313
                ),
1314
                point12_spacer,
1315
                doHeading("Table " + str(table_num) + ". Asset Alerts.", table),
1316
            ]
1317
        )
1318
    )
1319
    sub_section += 1
5✔
1320
    table_num += 1
5✔
1321
    Story.append(
5✔
1322
        format_table(
1323
            data_dict["asset_alerts"],
1324
            table_header,
1325
            [2 * inch, 3.5 * inch, 1 * inch],
1326
            [None, body, None],
1327
        )
1328
    )
1329

1330
    Story.append(point12_spacer)
5✔
1331
    Story.append(
5✔
1332
        KeepTogether(
1333
            [
1334
                Paragraph("3.4." + str(sub_section) + " Executive Alerts", h3),
1335
                Paragraph(
1336
                    """
1337
                    <font face="Franklin_Gothic_Medium_Regular">Table """
1338
                    + str(table_num)
1339
                    + """</font> includes discussions involving stakeholder
1340
                    executives and upper management.
1341
                """,
1342
                    body,
1343
                ),
1344
                point12_spacer,
1345
                doHeading("Table " + str(table_num) + ". Executive Alerts.", table),
1346
            ]
1347
        )
1348
    )
1349
    sub_section += 1
5✔
1350
    table_num += 1
5✔
1351
    Story.append(
5✔
1352
        format_table(
1353
            data_dict["alerts_exec"],
1354
            table_header,
1355
            [2 * inch, 3.5 * inch, 1 * inch],
1356
            [None, body, None],
1357
        )
1358
    )
1359

1360
    Story.append(point12_spacer)
5✔
1361

1362
    Story.append(
5✔
1363
        KeepTogether(
1364
            [
1365
                Paragraph("3.4." + str(sub_section) + " Threat Actors", h3),
1366
                Paragraph(
1367
                    """
1368
                    A threat actor's score is based on the amount of activity that person has on the dark web, the types of
1369
                    content posted, how prominent their account is on a forum, and if there is a larger circle of connections to
1370
                    other bad actors. Threat Actors are ranked 1 to 10, with 10 being the most severe.
1371
                    <font face="Franklin_Gothic_Medium_Regular">Table """
1372
                    + str(table_num)
1373
                    + """</font>
1374
                    identifies the top actors that have mentioned stakeholder assets.
1375
                """,
1376
                    body,
1377
                ),
1378
                point12_spacer,
1379
                doHeading("Table " + str(table_num) + ". Threat Actors.", table),
1380
            ]
1381
        )
1382
    )
1383
    sub_section += 1
5✔
1384
    table_num += 1
5✔
1385
    Story.append(
5✔
1386
        format_table(
1387
            data_dict["dark_web_actors"],
1388
            table_header,
1389
            [5.5 * inch, 1 * inch],
1390
            [body, None],
1391
        )
1392
    )
1393

1394
    Story.append(point12_spacer)
5✔
1395

1396
    Story.append(
5✔
1397
        KeepTogether(
1398
            [
1399
                Paragraph(
1400
                    "3.4." + str(sub_section) + " Alerts of Potential Threats", h3
1401
                ),
1402
                Paragraph(
1403
                    """
1404
                    Threats are derived by scanning suspicious chatter on the dark web that may have terms related to
1405
                    vulnerabilities. <font face="Franklin_Gothic_Medium_Regular">Table """
1406
                    + str(table_num)
1407
                    + """</font> identifies the most
1408
                    common threats.
1409
                """,
1410
                    body,
1411
                ),
1412
                point12_spacer,
1413
                doHeading(
1414
                    "Table " + str(table_num) + ". Alerts of Potential Threats.", table
1415
                ),
1416
            ]
1417
        )
1418
    )
1419
    sub_section += 1
5✔
1420
    table_num += 1
5✔
1421
    Story.append(
5✔
1422
        format_table(
1423
            data_dict["alerts_threats"],
1424
            table_header,
1425
            [2 * inch, 3.5 * inch, 1 * inch],
1426
            [None, body, None],
1427
        )
1428
    )
1429

1430
    Story.append(point12_spacer)
5✔
1431

1432
    Story.append(
5✔
1433
        KeepTogether(
1434
            [
1435
                Paragraph("3.4." + str(sub_section) + " Most Active Sites", h3),
1436
                Paragraph(
1437
                    """
1438
                    <font face="Franklin_Gothic_Medium_Regular">Table """
1439
                    + str(table_num)
1440
                    + """</font> includes the most active discussion forums where the organization is the topic of discussion.
1441
                """,
1442
                    body,
1443
                ),
1444
                point12_spacer,
1445
                doHeading("Table " + str(table_num) + ". Most Active Sites.", table),
1446
            ]
1447
        )
1448
    )
1449
    sub_section += 1
5✔
1450
    table_num += 1
5✔
1451
    Story.append(
5✔
1452
        format_table(
1453
            data_dict["dark_web_sites"],
1454
            table_header,
1455
            [5 * inch, 1.5 * inch],
1456
            [body, None],
1457
        )
1458
    )
1459

1460
    Story.append(point12_spacer)
5✔
1461

1462
    Story.append(
5✔
1463
        KeepTogether(
1464
            [
1465
                Paragraph("3.4." + str(sub_section) + " Invite-Only Market Alerts", h3),
1466
                Paragraph(
1467
                    """
1468
                    <font face="Franklin_Gothic_Medium_Regular">Table """
1469
                    + str(table_num)
1470
                    + """</font> includes the number of alerts on each invite-only
1471
                    market where compromised credentials were offered for sale.
1472
                """,
1473
                    body,
1474
                ),
1475
                point12_spacer,
1476
                doHeading(
1477
                    "Table " + str(table_num) + ". Invite-Only Market Alerts.", table
1478
                ),
1479
            ]
1480
        )
1481
    )
1482
    sub_section += 1
5✔
1483
    table_num += 1
5✔
1484
    Story.append(
5✔
1485
        format_table(
1486
            data_dict["markets_table"],
1487
            table_header,
1488
            [4 * inch, 2.5 * inch],
1489
            [None, None],
1490
        )
1491
    )
1492

1493
    Story.append(point12_spacer)
5✔
1494
    Story.append(
5✔
1495
        KeepTogether(
1496
            [
1497
                Paragraph(
1498
                    "3.4." + str(sub_section) + " Most Active CVEs on the Dark Web", h3
1499
                ),
1500
                Paragraph(
1501
                    """
1502
                    Rated by CyberSixGill's Dynamic Vulnerability Exploit (DVE) Score, this state-of-the-art machine
1503
                    learning model automatically predicts the probability of a CVE being exploited.
1504
                    <font face="Franklin_Gothic_Medium_Regular">Table """
1505
                    + str(table_num)
1506
                    + """</font> identifies the top 10 CVEs this report period.
1507
                """,
1508
                    body,
1509
                ),
1510
                point12_spacer,
1511
                doHeading(
1512
                    "Table " + str(table_num) + ". Most Active CVEs on the Dark Web.",
1513
                    table,
1514
                ),
1515
            ]
1516
        )
1517
    )
1518
    sub_section += 1
5✔
1519
    table_num += 1
5✔
1520
    Story.append(
5✔
1521
        format_table(
1522
            data_dict["top_cves"],
1523
            table_header,
1524
            [1.5 * inch, 3.5 * inch, 1.5 * inch],
1525
            [
1526
                None,
1527
                body,
1528
                None,
1529
            ],
1530
        )
1531
    )
1532

1533
    Story.append(point12_spacer)
5✔
1534

1535
    Story.append(NextPageTemplate("ContentPage"))
5✔
1536
    Story.append(PageBreak())
5✔
1537

1538
    # Start generating Methodology page
1539
    Story.append(doHeading("4. Methodology", h1))
5✔
1540
    Story.append(horizontal_line)
5✔
1541
    Story.append(point12_spacer)
5✔
1542
    Story.append(doHeading("4.1 Background", h2))
5✔
1543
    Story.append(
5✔
1544
        Paragraph(
1545
            """Cyber Hygiene's Posture and Exposure is a service provided by the Cybersecurity
1546
            and Infrastructure Security Agency (CISA).<br/><br/>
1547
            Cyber Hygiene started providing Posture and Exposure reports in October 2020 to assess,
1548
            on a recurring basis, the security posture of your organization by tracking dark web activity,
1549
            domain alerts, vulnerabilites, and credential exposures.""",
1550
            body,
1551
        )
1552
    )
1553
    Story.append(point12_spacer)
5✔
1554
    Story.append(doHeading("4.2 Process", h2))
5✔
1555
    Story.append(
5✔
1556
        Paragraph(
1557
            """Upon submission of an Acceptance Letter, DHS provided CISA with their
1558
            public network address information.<br/><br/>
1559
            The Posture and Exposure team uses this information to conduct investigations
1560
            with various open-source tools. Resulting data is then parsed for key-findings
1561
            and alerts. Summary data and detailed overviews are organized into this report
1562
            and packaged into an encrypted file for delivery.""",
1563
            body,
1564
        )
1565
    )
1566
    Story.append(point12_spacer)
5✔
1567
    Story.append(doHeading("5. Conclusion", h1))
5✔
1568
    Story.append(horizontal_line)
5✔
1569
    Story.append(point12_spacer)
5✔
1570
    Story.append(
5✔
1571
        Paragraph(
1572
            """Your organization should use the data provided in this report to correct any identified vulnerabilities,
1573
            exposures, or posture concerns. If you have any questions, comments, or concerns about the findings or data
1574
            contained in this report, please work with your designated technical point of contact when requesting
1575
            assistance from CISA at vulnerability@cisa.dhs.gov.""",
1576
            body,
1577
        )
1578
    )
1579
    Story.append(NextPageTemplate("ContentPage"))
5✔
1580
    Story.append(PageBreak())
5✔
1581

1582
    Story.append(doHeading("Appendix A: Additional Information", h1))
5✔
1583
    Story.append(horizontal_line)
5✔
1584
    Story.append(point12_spacer)
5✔
1585
    # If there are breaches print breach descriptions
1586
    if len(data_dict["breach_appendix"]) > 0:
5!
NEW
1587
        Story.append(Paragraph("Credential Breach Details: ", h2))
×
NEW
1588
        Story.append(Spacer(1, 6))
×
NEW
1589
        for row in data_dict["breach_appendix"].itertuples(index=False):
×
1590
            # Add anchor points for breach links
NEW
1591
            Story.append(
×
1592
                Paragraph(
1593
                    """
1594
                <a name="{link_name}"/><font face="Franklin_Gothic_Medium_Regular">{breach_name}</font>: {description}
1595
            """.format(
1596
                        breach_name=row[0],
1597
                        description=row[1].replace(' rel="noopener"', ""),
1598
                        link_name=sha256(str(row[0]).encode("utf8")).hexdigest(),
1599
                    ),
1600
                    body,
1601
                )
1602
            )
NEW
1603
            Story.append(point12_spacer)
×
NEW
1604
        Story.append(point12_spacer)
×
1605

1606
    # If there are verified vulns print summary info table
1607
    if len(data_dict["verif_vulns_summary"]) > 0:
5!
NEW
1608
        Story.append(Paragraph("Verified Vulnerability Summaries:", h2))
×
1609

NEW
1610
        Story.append(
×
1611
            Paragraph(
1612
                """Verified vulnerabilities are determined by the Shodan scanner and identify assets with active, known vulnerabilities. More information
1613
                about CVEs can be found <link href="https://nvd.nist.gov/">here</link>.""",
1614
                body,
1615
            )
1616
        )
NEW
1617
        Story.append(point12_spacer)
×
NEW
1618
        Story.append(
×
1619
            doHeading(
1620
                "Table " + str(table_num) + ". Verified Vulnerabilities Summaries.",
1621
                table,
1622
            )
1623
        )
1624
        # Add anchor points for vuln links
NEW
1625
        data_dict["verif_vulns_summary"]["CVE"] = (
×
1626
            '<a name="'
1627
            + data_dict["verif_vulns_summary"]["CVE"].str.replace("-", "_")
1628
            + '"/>'
1629
            + data_dict["verif_vulns_summary"]["CVE"].astype(str)
1630
        )
NEW
1631
        Story.append(
×
1632
            format_table(
1633
                data_dict["verif_vulns_summary"],
1634
                table_header,
1635
                [1.5 * inch, 1.25 * inch, 0.75 * inch, 3 * inch],
1636
                [body, None, None, body],
1637
            )
1638
        )
NEW
1639
        Story.append(point12_spacer)
×
1640

1641
    Story.append(
5✔
1642
        KeepTogether(
1643
            [
1644
                doHeading("Appendix B: Frequently Asked Questions", h1),
1645
                horizontal_line,
1646
                point12_spacer,
1647
                Paragraph(
1648
                    """<font face="Franklin_Gothic_Medium_Regular">How are P&E data and reports different from other reports I receive from CISA?</font><br/>
1649
            The Cybersecurity and Infrastructure Security Agency's (CISA) Cyber Hygiene Posture and Exposure (P&E)
1650
            analysis is a cost-free service that helps stakeholders monitor and evaluate their cyber posture for
1651
            weaknesses found in public source information, which is readily available to an attacker to view.
1652
            P&E utilizes passive reconnaissance services, dark web analysis, and other public information
1653
            sources to identify suspected domain masquerading, credentials that have been leaked or exposed,
1654
            insecure devices, suspected vulnerabilities, and increased dark web activity related to their organization.
1655
            """,
1656
                    body,
1657
                ),
1658
            ]
1659
        )
1660
    )
1661
    Story.append(point12_spacer)
5✔
1662
    Story.append(
5✔
1663
        Paragraph(
1664
            """<font face="Franklin_Gothic_Medium_Regular">What should I expect in terms of P&E's Findings? </font><br/>
1665
            The Posture and Exposure team uses numerous tools and open-source intelligence (OSINT) gathering tactics to
1666
            identify the potential weaknesses listed below. The data is then analyzed and complied into a Posture and
1667
            Exposure Report which provides both executive level information and detailed information for analysts that
1668
            includes the raw findings.""",
1669
            body,
1670
        )
1671
    )
1672
    Story.append(point12_spacer)
5✔
1673

1674
    Story.append(
5✔
1675
        Paragraph(
1676
            """
1677
            <font face="Franklin_Gothic_Medium_Regular">Suspected Domain Masquerading:</font><br/>
1678
            Spoofed or typo-squatting domains can be used to host fake web pages for malicious purposes, such as
1679
            imitating landing pages for spear phishing campaigns. This report shows newly registered or reactivated
1680
            domains that appear to mimic a stakeholder's actual domain.""",
1681
            indented,
1682
        )
1683
    )
1684

1685
    Story.append(
5✔
1686
        Paragraph(
1687
            """
1688
            <font face="Franklin_Gothic_Medium_Regular">Credentials Leaked/Exposed:</font><br/>
1689
            Credential leakage occurs when user credentials, often including passwords, are stolen via phishing campaigns,
1690
            network compromise, or misconfiguration of databases leading to public exposure. This leaked data is then listed
1691
            for sale on numerous forums and sites on the dark web, which provides attackers easy access to a stakeholders'
1692
            networks.
1693
        """,
1694
            indented,
1695
        )
1696
    )
1697

1698
    Story.append(
5✔
1699
        Paragraph(
1700
            """
1701
            <font face="Franklin_Gothic_Medium_Regular">Insecure Devices & Suspected Vulnerabilities:</font><br/>
1702
            When looking at Open-Source information gathered from tools that search the web for Internet of Things
1703
            (IoT) devices and other external facing assets. It can then be inferred that certain systems, ports, and
1704
            protocols associated with these assets are likely to have vulnerabilities, based on the OS or application
1705
            version information reported when queried. When possible, our analysis also reports on potential malware
1706
            infections for stakeholders.
1707
        """,
1708
            indented,
1709
        )
1710
    )
1711

1712
    Story.append(
5✔
1713
        KeepTogether(
1714
            Paragraph(
1715
                """
1716
                    <font face="Franklin_Gothic_Medium_Regular">Increased Dark Web Activity:</font><br/>
1717
                    Stakeholders and vulnerabilities are often discussed in various ways on the dark web. P&E monitors this
1718
                    activity, as well as the source (forums, websites, tutorials), and threat actors involved. A spike in
1719
                    activity can indicate a greater likelihood of an attack, vulnerability, or data leakage. Additionally,
1720
                    the urgency of the threat can be evaluated based on the threat actors involved along with other thresholds.
1721
                    Evaluating this content may also indicate if a stakeholder has been involved in a hacking incident as that data
1722
                    will often be published or offered 'for sale'. This information along with a list of the most active CVEs on the
1723
                    Dark Web may assist in prioritizing remediation activities.
1724

1725
                """,
1726
                indented,
1727
            )
1728
        )
1729
    )
1730

1731
    Story.append(
5✔
1732
        Paragraph(
1733
            """<font face="Franklin_Gothic_Medium_Regular">Do you perform scans of our networks?</font><br/>
1734
            P&E does not perform active scanning. The information we gather is through passive collection from numerous
1735
            public and vendor data sources. As such, we collect data on a continual basis, and provide summary reports
1736
            twice a month.
1737
        """,
1738
            body,
1739
        )
1740
    )
1741
    Story.append(point12_spacer)
5✔
1742

1743
    Story.append(
5✔
1744
        Paragraph(
1745
            """<font face="Franklin_Gothic_Medium_Regular">Do you perform scans of our networks?</font><br/>
1746
            P&E does not perform active scanning. The information we gather is through passive collection from numerous
1747
            public and vendor data sources. As such, we collect data on a continual basis, and provide summary reports
1748
            twice a month.
1749

1750
        """,
1751
            body,
1752
        )
1753
    )
1754
    Story.append(point12_spacer)
5✔
1755

1756
    Story.append(
5✔
1757
        Paragraph(
1758
            """<font face="Franklin_Gothic_Medium_Regular">How will the results be provided to me?</font><br/>
1759
            P&E will provide twice monthly P&E reports as password-protected attachments to emails from
1760
            vulnerability@cisa.dhs.gov. The attachments will contain a PDF—providing a summary of the findings,
1761
            tables, graphs, as charts—as well as a JSON file containing the raw data used to generate the PDF
1762
            report to facilitate your agencies own analysis.
1763
        """,
1764
            body,
1765
        )
1766
    )
1767
    Story.append(point12_spacer)
5✔
1768

1769
    Story.append(
5✔
1770
        Paragraph(
1771
            """<font face="Franklin_Gothic_Medium_Regular">Do you offer ad-hoc analysis of source data?</font><br/>
1772
            If you have any questions about a particular vulnerability that you believe you have mitigated, but
1773
            which continues to show up in the reports, we can perform a detailed analysis to determine why your
1774
            organization continues to show that vulnerability. In many cases, the issue can be tracked back to
1775
            the fact that the mitigation has made it impossible for the reconnaissance service or tool to identify
1776
            the configuration, and as such they may default to displaying the last collected information.
1777
        """,
1778
            body,
1779
        )
1780
    )
1781
    Story.append(point12_spacer)
5✔
1782

1783
    Story.append(
5✔
1784
        Paragraph(
1785
            """<font face="Franklin_Gothic_Medium_Regular">Who do I contact if there are any issues or updates that need to be addressed for my reports?</font><br/>
1786
            The general notification process is the same as all of the CyHy components. Simply send an email to
1787
            vulnerability@cisa.dhs.gov identifying the requested changes. In this instance, make sure to identify
1788
            “P&E Report Delivery” in the subject to ensure the issue is routed to our team.
1789
        """,
1790
            body,
1791
        )
1792
    )
1793
    Story.append(point12_spacer)
5✔
1794
    Story.append(
5✔
1795
        KeepTogether(
1796
            [
1797
                doHeading("Appendix C: Acronyms", h1),
1798
                horizontal_line,
1799
                point12_spacer,
1800
                Table(
1801
                    [
1802
                        ["CISA", "Cybersecurity and Infrastructure Security Agency"],
1803
                        ["CVE", "Common Vulnerabilities and Exposures"],
1804
                        ["DHS", "Department of Homeland Security"],
1805
                        ["DVE", "Dynamic Vulnerability Exploit"],
1806
                        ["FTP", "File Transfer Protocol"],
1807
                        ["HTTP", "Hypertext Transfer Protocol"],
1808
                        ["IP", "Internet Protocol"],
1809
                        ["P&E", "Posture and Exposure"],
1810
                        ["RDP", "Remote Desktop Protocol"],
1811
                        ["SIP", "Session Initiation Protocol"],
1812
                        ["SMB", "Server Message Block"],
1813
                    ]
1814
                ),
1815
            ]
1816
        )
1817
    )
1818
    doc.multiBuild(Story)
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc