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

cisagov / pe-reports / 5892227966

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

Pull #565

github

web-flow
Merge 9adf19bbe 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
            # use max(leftPadding,rightPadding)
903
            name="domain_masq_kpis",  # for identification purposes when stuff goes awry
904
            endSlack=0.1,  # height disparity allowance ie 10% of available height
905
        )
906
    )
907

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

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

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

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

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

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

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

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

1113
    Story.append(point12_spacer)
5✔
1114

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

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

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

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

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

1192
    Story.append(point12_spacer)
5✔
1193

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

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

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

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

1298
    Story.append(point12_spacer)
5✔
1299

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

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

1359
    Story.append(point12_spacer)
5✔
1360

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

1393
    Story.append(point12_spacer)
5✔
1394

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

1429
    Story.append(point12_spacer)
5✔
1430

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

1459
    Story.append(point12_spacer)
5✔
1460

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

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

1532
    Story.append(point12_spacer)
5✔
1533

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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