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

cisagov / pe-reports / 5892417240

17 Aug 2023 02:44PM UTC coverage: 33.736% (+7.0%) from 26.737%
5892417240

Pull #565

github

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

93 of 477 branches covered (19.5%)

Branch coverage included in aggregate %.

443 of 1022 new or added lines in 8 files covered. (43.35%)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

968
    Story.append(PageBreak())
5✔
969

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

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

1073
    Story.append(point12_spacer)
5✔
1074
    Story.append(
5✔
1075
        KeepTogether(
1076
            [
1077
                Paragraph("3.3.2 Shodan-Verified Vulnerabilities", h3),
1078
                Paragraph(
1079
                    """
1080
                    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
1081
                    through extra checks to validate the finding. Refer to Appendix A for summary data.
1082
                """,
1083
                    body,
1084
                ),
1085
                doHeading(
1086
                    """
1087
                    Table 5. Shodan-Verified Vulnerabilities.
1088
                """,
1089
                    table,
1090
                ),
1091
            ]
1092
        )
1093
    )
1094
    # add link to appendix for CVE string
1095
    data_dict["verif_vulns"]["CVE"] = (
5✔
1096
        '<link href="#'
1097
        + data_dict["verif_vulns"]["CVE"].str.replace("-", "_")
1098
        + '" color="#003e67">'
1099
        + data_dict["verif_vulns"]["CVE"].astype(str)
1100
        + "</link>"
1101
    )
1102

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

1112
    Story.append(point12_spacer)
5✔
1113

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

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

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

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

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

1191
    Story.append(point12_spacer)
5✔
1192

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

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

1256
        Story.append(point12_spacer)
5✔
1257
        sub_section = 3
5✔
1258
        table_num = 7
5✔
1259

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

1297
    Story.append(point12_spacer)
5✔
1298

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

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

1358
    Story.append(point12_spacer)
5✔
1359

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

1392
    Story.append(point12_spacer)
5✔
1393

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

1428
    Story.append(point12_spacer)
5✔
1429

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

1458
    Story.append(point12_spacer)
5✔
1459

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

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

1531
    Story.append(point12_spacer)
5✔
1532

1533
    Story.append(NextPageTemplate("ContentPage"))
5✔
1534
    Story.append(PageBreak())
5✔
1535

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

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

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

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

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

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

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

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

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

1723
                """,
1724
                indented,
1725
            )
1726
        )
1727
    )
1728

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

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

1748
        """,
1749
            body,
1750
        )
1751
    )
1752
    Story.append(point12_spacer)
5✔
1753

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

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

1781
    Story.append(
5✔
1782
        Paragraph(
1783
            """<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/>
1784
            The general notification process is the same as all of the CyHy components. Simply send an email to
1785
            vulnerability@cisa.dhs.gov identifying the requested changes. In this instance, make sure to identify
1786
            “P&E Report Delivery” in the subject to ensure the issue is routed to our team.
1787
        """,
1788
            body,
1789
        )
1790
    )
1791
    Story.append(point12_spacer)
5✔
1792
    Story.append(
5✔
1793
        KeepTogether(
1794
            [
1795
                doHeading("Appendix C: Acronyms", h1),
1796
                horizontal_line,
1797
                point12_spacer,
1798
                Table(
1799
                    [
1800
                        ["CISA", "Cybersecurity and Infrastructure Security Agency"],
1801
                        ["CVE", "Common Vulnerabilities and Exposures"],
1802
                        ["DHS", "Department of Homeland Security"],
1803
                        ["DVE", "Dynamic Vulnerability Exploit"],
1804
                        ["FTP", "File Transfer Protocol"],
1805
                        ["HTTP", "Hypertext Transfer Protocol"],
1806
                        ["IP", "Internet Protocol"],
1807
                        ["P&E", "Posture and Exposure"],
1808
                        ["RDP", "Remote Desktop Protocol"],
1809
                        ["SIP", "Session Initiation Protocol"],
1810
                        ["SMB", "Server Message Block"],
1811
                    ]
1812
                ),
1813
            ]
1814
        )
1815
    )
1816
    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