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

cisagov / pe-reports / 5883245823

16 Aug 2023 07:57PM UTC coverage: 33.736% (+7.0%) from 26.737%
5883245823

Pull #565

github

web-flow
Capitalize filetype names

Capitalize filetype names in comment

Co-authored-by: dav3r <david.redmin@trio.dhs.gov>
Pull Request #565: Update report generator to use reportlab

90 of 474 branches covered (18.99%)

Branch coverage included in aggregate %.

447 of 1026 new or added lines in 8 files covered. (43.57%)

18 existing lines in 5 files now uncovered.

804 of 2176 relevant lines covered (36.95%)

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
pdfmetrics.registerFont(
5✔
40
    TTFont(
41
        "Franklin_Gothic_Book_Italic", BASE_DIR + "/fonts/FranklinGothicBookItalic.ttf"
42
    )
43
)
44
pdfmetrics.registerFont(
5✔
45
    TTFont("Franklin_Gothic_Book", BASE_DIR + "/fonts/FranklinGothicBook.ttf")
46
)
47
pdfmetrics.registerFont(
5✔
48
    TTFont(
49
        "Franklin_Gothic_Demi_Regular",
50
        BASE_DIR + "/fonts/FranklinGothicDemiRegular.ttf",
51
    )
52
)
53
pdfmetrics.registerFont(
5✔
54
    TTFont(
55
        "Franklin_Gothic_Medium_Italic",
56
        BASE_DIR + "/fonts/FranklinGothicMediumItalic.ttf",
57
    )
58
)
59
pdfmetrics.registerFont(
5✔
60
    TTFont(
61
        "Franklin_Gothic_Medium_Regular",
62
        BASE_DIR + "/fonts/FranklinGothicMediumRegular.ttf",
63
    )
64
)
65

66
defaultPageSize = letter
5✔
67
PAGE_HEIGHT = defaultPageSize[1]
5✔
68
PAGE_WIDTH = defaultPageSize[0]
5✔
69

70

71
def sha_hash(s: str):
5✔
72
    """Hash a given string."""
NEW
73
    return sha256(s.encode("utf-8")).hexdigest()
×
74

75

76
# Extend Table of contents to create a List of Figures Class
77
class ListOfFigures(TableOfContents):
5✔
78
    """Class extention to build a Table of Figures."""
79

80
    def notify(self, kind, stuff):
5✔
81
        """
82
        Call he notification hook to register all kinds of events.
83

84
        Here we are interested in 'Figure' events only.
85
        """
86
        if kind == "TOCFigure":
5✔
87
            self.addEntry(*stuff)
5✔
88

89

90
# Extend Table of contents to create a List of Tables Class
91
class ListOfTables(TableOfContents):
5✔
92
    """Class extention to build a Table of Tables."""
93

94
    def notify(self, kind, stuff):
5✔
95
        """Call the notification hook to register all kinds of events.
96

97
        Here we are interested in 'Table' events only.
98
        """
99
        if kind == "TOCTable":
5✔
100
            self.addEntry(*stuff)
5✔
101

102

103
class MyDocTemplate(BaseDocTemplate):
5✔
104
    """Extend the BaseDocTemplate to adjust Template."""
105

106
    def __init__(self, filename, **kw):
5✔
107
        """Initialize MyDocTemplate."""
108
        self.allowSplitting = 0
5✔
109
        BaseDocTemplate.__init__(self, filename, **kw)
5✔
110
        self.pagesize = defaultPageSize
5✔
111

112
    def afterFlowable(self, flowable):
5✔
113
        """Register TOC, TOT, and TOF entries."""
114
        if flowable.__class__.__name__ == "Paragraph":
5✔
115
            text = flowable.getPlainText()
5✔
116
            style = flowable.style.name
5✔
117
            if style == "Heading1":
5✔
118
                level = 0
5✔
119
                notification = "TOCEntry"
5✔
120
            elif style == "Heading2":
5✔
121
                level = 1
5✔
122
                notification = "TOCEntry"
5✔
123
            elif style == "figure":
5✔
124
                level = 0
5✔
125
                notification = "TOCFigure"
5✔
126
            elif style == "table":
5✔
127
                level = 0
5✔
128
                notification = "TOCTable"
5✔
129
            else:
130
                return
5✔
131
            E = [level, text, self.page]
5✔
132
            # if we have a bookmark name, append that to our notify data
133
            bn = getattr(flowable, "_bookmarkName", None)
5✔
134
            if bn is not None:
5!
135
                E.append(bn)
5✔
136
            self.notify(notification, tuple(E))
5✔
137

138

139
class ConditionalSpacer(Spacer):
5✔
140
    """Create a Conditional Spacer class."""
141

142
    def wrap(self, availWidth, availHeight):
5✔
143
        """Create a spacer if there is space on the page to do so."""
144
        height = min(self.height, availHeight - 1e-8)
5✔
145
        return (availWidth, height)
5✔
146

147

148
def get_image(path, width=1 * inch):
5✔
149
    """Read in an image and scale it based on the width argument."""
150
    img = utils.ImageReader(path)
5✔
151
    iw, ih = img.getSize()
5✔
152
    aspect = ih / float(iw)
5✔
153
    return Image(path, width=width, height=(width * aspect))
5✔
154

155

156
def format_table(
5✔
157
    df, header_style, column_widths, column_style_list, remove_symbols=False
158
):
159
    """Read in a dataframe and convert it to a table and format it with a provided style list."""
160
    header_row = [
5✔
161
        [Paragraph(str(cell), header_style) for cell in row] for row in [df.columns]
162
    ]
163
    data = []
5✔
164
    for row in np.array(df).tolist():
5!
NEW
165
        current_cell = 0
×
NEW
166
        current_row = []
×
NEW
167
        for cell in row:
×
NEW
168
            if column_style_list[current_cell] is not None:
×
169
                # Remove emojis from content because the report generator can't display them
NEW
170
                cell = Paragraph(
×
171
                    demoji.replace(str(cell), ""), column_style_list[current_cell]
172
                )
173

NEW
174
            current_row.append(cell)
×
NEW
175
            current_cell += 1
×
NEW
176
        data.append(current_row)
×
177

178
    data = header_row + data
5✔
179

180
    table = Table(
5✔
181
        data,
182
        colWidths=column_widths,
183
        rowHeights=None,
184
        style=None,
185
        splitByRow=1,
186
        repeatRows=1,
187
        repeatCols=0,
188
        rowSplitRange=(2, -1),
189
        spaceBefore=None,
190
        spaceAfter=None,
191
        cornerRadii=None,
192
    )
193

194
    style = TableStyle(
5✔
195
        [
196
            ("VALIGN", (0, 0), (-1, 0), "MIDDLE"),
197
            ("ALIGN", (0, 0), (-1, -1), "CENTER"),
198
            ("VALIGN", (0, 1), (-1, -1), "MIDDLE"),
199
            ("INNERGRID", (0, 0), (-1, -1), 1, "white"),
200
            ("TEXTFONT", (0, 1), (-1, -1), "Franklin_Gothic_Book"),
201
            ("FONTSIZE", (0, 1), (-1, -1), 12),
202
            (
203
                "ROWBACKGROUNDS",
204
                (0, 1),
205
                (-1, -1),
206
                [HexColor("#FFFFFF"), HexColor("#DEEBF7")],
207
            ),
208
            ("BACKGROUND", (0, 0), (-1, 0), HexColor("#1d5288")),
209
            ("LINEBELOW", (0, -1), (-1, -1), 1.5, HexColor("#1d5288")),
210
        ]
211
    )
212
    table.setStyle(style)
5✔
213

214
    if len(df) == 0:
5!
215
        label = Paragraph(
5✔
216
            "No Data to Report",
217
            ParagraphStyle(
218
                name="centered",
219
                fontName="Franklin_Gothic_Medium_Regular",
220
                textColor=HexColor("#a7a7a6"),
221
                fontSize=16,
222
                leading=16,
223
                alignment=1,
224
                spaceAfter=10,
225
                spaceBefore=10,
226
            ),
227
        )
228
        table = KeepTogether([table, label])
5✔
229
    return table
5✔
230

231

232
def build_kpi(data, width):
5✔
233
    """Build a KPI element."""
234
    table = Table(
5✔
235
        [[data]],
236
        colWidths=[width * inch],
237
        rowHeights=60,
238
        style=None,
239
        splitByRow=1,
240
        repeatRows=0,
241
        repeatCols=0,
242
        rowSplitRange=None,
243
        spaceBefore=None,
244
        spaceAfter=None,
245
        cornerRadii=[10, 10, 10, 10],
246
    )
247

248
    style = TableStyle(
5✔
249
        [
250
            ("VALIGN", (0, 0), (-1, 0), "MIDDLE"),
251
            ("ALIGN", (0, 0), (-1, -1), "CENTER"),
252
            ("VALIGN", (0, 1), (-1, -1), "MIDDLE"),
253
            ("GRID", (0, 0), (0, 0), 1, HexColor("#003e67")),
254
            ("BACKGROUND", (0, 0), (0, 0), HexColor("#DEEBF7")),
255
        ]
256
    )
257
    table.setStyle(style)
5✔
258
    return table
5✔
259

260

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

264
    def titlePage(canvas, doc):
5✔
265
        """Build static elements of the cover page."""
266
        canvas.saveState()
5✔
267
        canvas.drawImage(BASE_DIR + "/assets/Cover.png", 0, 0, width=None, height=None)
5✔
268
        canvas.setFont("Franklin_Gothic_Medium_Regular", 32)
5✔
269
        canvas.drawString(50, 660, "Posture & Exposure Report")
5✔
270
        canvas.restoreState()
5✔
271

272
    def summaryPage(canvas, doc):
5✔
273
        """Build static elements of the summary page."""
274
        canvas.saveState()
5✔
275
        canvas.setFont("Franklin_Gothic_Book", 13)
5✔
276
        canvas.drawImage(
5✔
277
            BASE_DIR + "/assets/summary-background.png",
278
            0,
279
            0,
280
            width=PAGE_WIDTH,
281
            height=PAGE_HEIGHT,
282
        )
283
        canvas.setFillColor(HexColor("#1d5288"))
5✔
284
        canvas.setStrokeColor("#1d5288")
5✔
285
        canvas.rect(inch, 210, 3.5 * inch, 5.7 * inch, fill=1)
5✔
286
        canvas.restoreState()
5✔
287
        summary_frame = Frame(
5✔
288
            1.1 * inch, 224, 3.3 * inch, 5.5 * inch, id=None, showBoundary=0
289
        )
290
        summary_1_style = ParagraphStyle(
5✔
291
            "summary_1_style",
292
            fontSize=12,
293
            alignment=0,
294
            textColor="white",
295
            fontName="Franklin_Gothic_Book",
296
        )
297
        summary_1 = Paragraph(
5✔
298
            """
299
        <font face="Franklin_Gothic_Medium_Regular">Credential Publication & Abuse:</font><br/>
300
        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.
301
        <br/><br/><br/><br/>
302
        <font face="Franklin_Gothic_Medium_Regular">Suspected Domain Masquerading Attempt:</font><br/>
303
        Registered domain names that are similar to legitimate domains which attempt to trick users into navigating to illegitimate domains.
304
        <br/><br/><br/><br/><br/><br/>
305
        <font face="Franklin_Gothic_Medium_Regular">Insecure Devices & Vulnerabilities:</font><br/>
306
        Open ports, risky protocols, insecure products, and externally observable vulnerabilities are potential targets for exploit.
307
        <br/><br/><br/><br/><br/>
308
        <font face="Franklin_Gothic_Medium_Regular">Dark Web Activity:</font><br/>
309
        Heightened public attention can indicate increased targeting and attack coordination, especially when attention is found on the dark web.
310
        """,
311
            style=summary_1_style,
312
        )
313
        summary_frame.addFromList([summary_1], canvas)
5✔
314

315
        summary_frame_2 = Frame(
5✔
316
            5.1 * inch, 552, 2.4 * inch, 0.7 * inch, id=None, showBoundary=0
317
        )
318
        summary_2 = Paragraph(
5✔
319
            str(data_dict["creds"])
320
            + """<br/> <font face="Franklin_Gothic_Book" size='10'>Total Credential Publications</font>""",
321
            style=kpi,
322
        )
323
        summary_frame_2.addFromList([summary_2], canvas)
5✔
324

325
        summary_frame_3 = Frame(
5✔
326
            5.1 * inch, 444, 2.4 * inch, 0.7 * inch, id=None, showBoundary=0
327
        )
328
        summary_3 = Paragraph(
5✔
329
            str(data_dict["suspectedDomains"])
330
            + """<br/> <font face="Franklin_Gothic_Book" size='10'>Suspected Domain Masquerading</font>""",
331
            style=kpi,
332
        )
333
        summary_frame_3.addFromList([summary_3], canvas)
5✔
334

335
        summary_frame_4 = Frame(
5✔
336
            5.1 * inch, 337, 2.4 * inch, 0.7 * inch, id=None, showBoundary=0
337
        )
338
        summary_4 = Paragraph(
5✔
339
            str(data_dict["verifVulns"])
340
            + """<br/> <font face="Franklin_Gothic_Book" size='10'>Shodan Verified Vulnerabilities Found</font>""",
341
            style=kpi,
342
        )
343
        summary_frame_4.addFromList([summary_4], canvas)
5✔
344

345
        summary_frame_5 = Frame(
5✔
346
            5.1 * inch, 230, 2.4 * inch, 0.7 * inch, id=None, showBoundary=0
347
        )
348
        summary_5 = Paragraph(
5✔
349
            str(data_dict["darkWeb"])
350
            + """<br/> <font face="Franklin_Gothic_Book" size='10'>Dark Web Alerts</font>""",
351
            style=kpi,
352
        )
353
        summary_frame_5.addFromList([summary_5], canvas)
5✔
354

355
        json_title_frame = Frame(
5✔
356
            3.85 * inch, 175, 1.5 * inch, 0.5 * inch, id=None, showBoundary=0
357
        )
358
        json_title = Paragraph(
5✔
359
            "JSON&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;EXCEL",
360
            style=json_excel,
361
        )
362
        json_title_frame.addFromList([json_title], canvas)
5✔
363

364
        canvas.setStrokeColor("#a7a7a6")
5✔
365
        canvas.setFillColor("#a7a7a6")
5✔
366
        canvas.drawInlineImage(
5✔
367
            BASE_DIR + "/assets/cisa.png", 45, 705, width=65, height=65
368
        )
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

380
    def contentPage(canvas, doc):
5✔
381
        """Build the header and footer content for the rest of the pages in the report."""
382
        canvas.saveState()
5✔
383
        canvas.setFont("Franklin_Gothic_Book", 12)
5✔
384
        canvas.setStrokeColor("#a7a7a6")
5✔
385
        canvas.setFillColor("#a7a7a6")
5✔
386
        canvas.drawImage(BASE_DIR + "/assets/cisa.png", 45, 705, width=65, height=65)
5✔
387
        canvas.drawString(130, 745, "Posture and Exposure Report")
5✔
388
        canvas.drawString(130, 725, "Reporting Period: " + data_dict["dateRange"])
5✔
389
        canvas.line(130, 710, PAGE_WIDTH - inch, 710)
5✔
390
        canvas.drawRightString(
5✔
391
            PAGE_WIDTH - inch, 0.75 * inch, "P&E Report | Page %d" % (doc.page)
392
        )
393
        canvas.drawString(inch, 0.75 * inch, data_dict["endDate"])
5✔
394
        canvas.setFont("Franklin_Gothic_Medium_Regular", 12)
5✔
395
        canvas.setFillColor("#FFC000")
5✔
396
        canvas.drawString(6.4 * inch, 745, "TLP: AMBER")
5✔
397
        canvas.restoreState()
5✔
398

399
    def doHeading(text, sty):
5✔
400
        """Add a bookmark to heading element to allow linking from the table of contents."""
401
        # create bookmarkname
402
        bn = sha256((text + sty.name).encode("utf8")).hexdigest()
5✔
403
        # modify paragraph text to include an anchor point with name bn
404
        h = Paragraph(text + '<a name="%s"/>' % bn, sty)
5✔
405
        # store the bookmark name on the flowable so afterFlowable can see this
406
        h._bookmarkName = bn
5✔
407
        return h
5✔
408

409
    # ***Document Structures***#
410
    """Build frames for different page structures."""
1✔
411
    doc = MyDocTemplate(data_dict["filename"])
5✔
412
    title_frame = Frame(45, 390, 530, 250, id=None, showBoundary=0)
5✔
413
    frameT = Frame(
5✔
414
        doc.leftMargin,
415
        doc.bottomMargin,
416
        PAGE_WIDTH - (2 * inch),
417
        PAGE_HEIGHT - (2.4 * inch),
418
        id="normal",
419
        showBoundary=0,
420
    )
421
    doc.addPageTemplates(
5✔
422
        [
423
            PageTemplate(id="TitlePage", frames=title_frame, onPage=titlePage),
424
            PageTemplate(id="SummaryPage", frames=frameT, onPage=summaryPage),
425
            PageTemplate(id="ContentPage", frames=frameT, onPage=contentPage),
426
        ]
427
    )
428
    Story = []
5✔
429
    """Build table of contents."""
1✔
430
    toc = TableOfContents()
5✔
431
    tof = ListOfFigures()
5✔
432
    tot = ListOfTables()
5✔
433

434
    """Create font and formatting styles."""
1✔
435
    PS = ParagraphStyle
5✔
436

437
    centered = PS(
5✔
438
        name="centered",
439
        fontName="Franklin_Gothic_Medium_Regular",
440
        fontSize=20,
441
        leading=16,
442
        alignment=1,
443
        spaceAfter=10,
444
        spaceBefore=10,
445
    )
446

447
    indented = PS(
5✔
448
        name="indented",
449
        fontName="Franklin_Gothic_Book",
450
        fontSize=12,
451
        leading=14,
452
        leftIndent=30,
453
        spaceAfter=20,
454
    )
455

456
    h1 = PS(
5✔
457
        fontName="Franklin_Gothic_Medium_Regular",
458
        name="Heading1",
459
        fontSize=16,
460
        leading=18,
461
        textColor=HexColor("#003e67"),
462
    )
463

464
    h2 = PS(
5✔
465
        name="Heading2",
466
        fontName="Franklin_Gothic_Medium_Regular",
467
        fontSize=14,
468
        leading=10,
469
        textColor=HexColor("#003e67"),
470
        spaceAfter=12,
471
    )
472

473
    h3 = PS(
5✔
474
        name="Heading3",
475
        fontName="Franklin_Gothic_Medium_Regular",
476
        fontSize=14,
477
        leading=10,
478
        textColor=HexColor("#003e67"),
479
        spaceAfter=10,
480
    )
481

482
    body = PS(
5✔
483
        name="body",
484
        leading=14,
485
        fontName="Franklin_Gothic_Book",
486
        fontSize=12,
487
    )
488

489
    kpi = PS(
5✔
490
        name="kpi",
491
        fontName="Franklin_Gothic_Medium_Regular",
492
        fontSize=14,
493
        leading=16,
494
        alignment=1,
495
        spaceAfter=20,
496
    )
497

498
    json_excel = PS(
5✔
499
        name="json_excel",
500
        fontName="Franklin_Gothic_Medium_Regular",
501
        fontSize=10,
502
        alignment=1,
503
    )
504

505
    figure = PS(
5✔
506
        name="figure",
507
        fontName="Franklin_Gothic_Medium_Regular",
508
        fontSize=12,
509
        leading=16,
510
        alignment=1,
511
    )
512

513
    table = PS(
5✔
514
        name="table",
515
        fontName="Franklin_Gothic_Medium_Regular",
516
        fontSize=12,
517
        leading=16,
518
        alignment=1,
519
        spaceAfter=12,
520
    )
521

522
    table_header = PS(
5✔
523
        name="table_header",
524
        fontName="Franklin_Gothic_Medium_Regular",
525
        fontSize=12,
526
        leading=16,
527
        alignment=1,
528
        spaceAfter=12,
529
        textColor=HexColor("#FFFFFF"),
530
    )
531

532
    title_data = PS(
5✔
533
        fontName="Franklin_Gothic_Medium_Regular", name="Title", fontSize=18, leading=20
534
    )
535

536
    """Stream all the dynamic content to the report."""
1✔
537

538
    # *************************#
539
    # Create repeated elements
540
    point12_spacer = ConditionalSpacer(1, 12)
5✔
541
    horizontal_line = HRFlowable(
5✔
542
        width="100%",
543
        thickness=1.5,
544
        lineCap="round",
545
        color=HexColor("#003e67"),
546
        spaceBefore=0,
547
        spaceAfter=1,
548
        hAlign="LEFT",
549
        vAlign="TOP",
550
        dash=None,
551
    )
552
    # ***Title Page***#
553
    Story.append(Paragraph("Prepared for: " + data_dict["department"], title_data))
5✔
554
    Story.append(point12_spacer)
5✔
555
    Story.append(Paragraph("Reporting Period: " + data_dict["dateRange"], title_data))
5✔
556
    Story.append(NextPageTemplate("ContentPage"))
5✔
557
    Story.append(PageBreak())
5✔
558

559
    # ***Table of Contents***#
560
    Story.append(Paragraph("<b>Table of Contents</b>", centered))
5✔
561
    # Set styles for levels in Table of contents
562
    toc_styles = [
5✔
563
        PS(
564
            fontName="Franklin_Gothic_Medium_Regular",
565
            fontSize=14,
566
            name="TOCHeading1",
567
            leftIndent=20,
568
            firstLineIndent=-20,
569
            spaceBefore=1,
570
            leading=14,
571
        ),
572
        PS(
573
            fontSize=12,
574
            name="TOCHeading2",
575
            leftIndent=40,
576
            firstLineIndent=-20,
577
            spaceBefore=0,
578
            leading=12,
579
        ),
580
        PS(
581
            fontSize=10,
582
            name="TOCHeading3",
583
            leftIndent=60,
584
            firstLineIndent=-20,
585
            spaceBefore=0,
586
            leading=12,
587
        ),
588
        PS(
589
            fontSize=10,
590
            name="TOCHeading4",
591
            leftIndent=100,
592
            firstLineIndent=-20,
593
            spaceBefore=0,
594
            leading=12,
595
        ),
596
    ]
597
    toc.levelStyles = toc_styles
5✔
598
    Story.append(toc)
5✔
599
    Story.append(PageBreak())
5✔
600

601
    # ***Table of Figures and Table of Contents***#
602
    tot.levelStyles = toc_styles
5✔
603
    tof.levelStyles = toc_styles
5✔
604
    Story.append(Paragraph("<b>Table of Figures</b>", centered))
5✔
605
    Story.append(tof)
5✔
606
    Story.append(Paragraph("<b>Table of Tables</b>", centered))
5✔
607
    Story.append(tot)
5✔
608
    Story.append(PageBreak())
5✔
609

610
    # ***Content Pages***#
611
    # ***Start Introduction Page***#
612
    Story.append(doHeading("1. Introduction", h1))
5✔
613
    Story.append(horizontal_line)
5✔
614
    Story.append(point12_spacer)
5✔
615
    Story.append(doHeading("1.1 Overview", h2))
5✔
616
    Story.append(
5✔
617
        Paragraph(
618
            """Posture and Exposure (P&E) offers stakeholders an opportunity to view their organizational
619
                risk from the viewpoint of the adversary. We utilize passive reconnaissance services,
620
                dark web analysis, and open-source tools to identify spoofing in order to generate a risk
621
                    profile report that is delivered on a regular basis.<br/><br/>
622
                As a customer of P&E you are receiving our regularly scheduled report which contains a
623
                summary of the activity we have been tracking on your behalf for the following services:
624
                <br/><br/>""",
625
            body,
626
        )
627
    )
628

629
    Story.append(
5✔
630
        ListFlowable(
631
            [
632
                ListItem(
633
                    Paragraph("Domain Masquerading and Monitoring", body),
634
                    leftIndent=35,
635
                    value="bulletchar",
636
                ),
637
                ListItem(
638
                    Paragraph("Vulnerabilities & Malware Associations", body),
639
                    leftIndent=35,
640
                    value="bulletchar",
641
                ),
642
                ListItem(
643
                    Paragraph("Dark Web Monitoring", body),
644
                    leftIndent=35,
645
                    value="bulletchar",
646
                ),
647
                ListItem(
648
                    Paragraph("Hidden Assets and Risky Services", body),
649
                    leftIndent=35,
650
                    value="bulletchar",
651
                ),
652
            ],
653
            bulletType="bullet",
654
            start="bulletchar",
655
            leftIndent=10,
656
        )
657
    )
658

659
    Story.append(
5✔
660
        Paragraph(
661
            """<br/>It is important to note that these findings have not been verified; everything is
662
                            gathered via passive analysis of publicly available sources. As such there may be false
663
                            positive findings; however, these findings should be treated as information that your
664
                            organization is leaking out to the internet for adversaries to notice.<br/><br/>""",
665
            body,
666
        )
667
    )
668

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

722
    Story.append(doHeading("1.3 Contact Information", h2))
5✔
723
    Story.append(
5✔
724
        Paragraph("Posture and Exposure Team Email: vulnerability@cisa.dhs.gov", body)
725
    )
726

727
    Story.append(NextPageTemplate("SummaryPage"))
5✔
728
    Story.append(PageBreak())
5✔
729

730
    # ***Start Generating Summary Page***#
731
    Story.append(doHeading("2. Summary of Findings", h1))
5✔
732
    Story.append(horizontal_line)
5✔
733
    Story.append(point12_spacer)
5✔
734
    Story.append(doHeading("2.1 Summary of Tracked Data", h2))
5✔
735
    Story.append(Spacer(1, 425))
5✔
736
    Story.append(doHeading("2.2 Raw Data Links", h2))
5✔
737
    Story.append(
5✔
738
        Paragraph(
739
            "Exposed Credentials<br/><br/>Domain Masquerading and Monitoring<br/><br/>Vulnerabilities and Malware Associations<br/><br/>Dark Web Activity",
740
            body,
741
        )
742
    )
743

744
    Story.append(NextPageTemplate("ContentPage"))
5✔
745
    Story.append(PageBreak())
5✔
746

747
    # ***Start Generating Creds Page***#
748
    Story.append(doHeading("3. Detailed Results", h1))
5✔
749
    Story.append(horizontal_line)
5✔
750
    Story.append(point12_spacer)
5✔
751
    Story.append(doHeading("3.1 Credential Publication and Abuse", h2))
5✔
752
    Story.append(
5✔
753
        Paragraph(
754
            """Credential leakage occurs when user credentials, often including passwords, are stolen via phishing
755
        campaigns, network compromise, or database misconfigurations leading to public exposure. This leaked data is
756
        then listed for sale on numerous forums and sites on the dark web which provides attackers easy access to a
757
        stakeholder's networks. Detailed results are presented below.
758
        """,
759
            body,
760
        )
761
    )
762

763
    # Build row of kpi cells
764
    row = [
5✔
765
        build_kpi(
766
            Paragraph(
767
                str(data_dict["breach"])
768
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Distinct Breaches</font>""",
769
                style=kpi,
770
            ),
771
            2,
772
        ),
773
        build_kpi(
774
            Paragraph(
775
                str(data_dict["creds"])
776
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Credentials Exposed</font>""",
777
                style=kpi,
778
            ),
779
            2,
780
        ),
781
        build_kpi(
782
            Paragraph(
783
                str(data_dict["pw_creds"])
784
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Credentials with Password</font>""",
785
                style=kpi,
786
            ),
787
            2,
788
        ),
789
    ]
790
    Story.append(
5✔
791
        BalancedColumns(
792
            row,  # the flowables we are balancing
793
            nCols=3,  # the number of columns
794
            needed=55,  # the minimum space needed by the flowable
795
            spaceBefore=0,
796
            spaceAfter=12,
797
            showBoundary=False,  # optional boundary showing
798
            leftPadding=4,  # these override the created frame
799
            rightPadding=0,  # paddings if specified else the
800
            topPadding=None,  # default frame paddings
801
            bottomPadding=None,  # are used
802
            innerPadding=8,  # the gap between frames if specified else
803
            # use max(leftPadding,rightPadding)
804
            name="creds_kpis",  # for identification purposes when stuff goes awry
805
            endSlack=0.1,  # height disparity allowance ie 10% of available height
806
        )
807
    )
808

809
    Story.append(
5✔
810
        Paragraph(
811
            """
812
            <font face="Franklin_Gothic_Medium_Regular">Figure 1</font> shows the credentials exposed during each week of the reporting period, including those with no
813
            passwords as well as those with passwords included.
814
        """,
815
            body,
816
        )
817
    )
818
    Story.append(point12_spacer)
5✔
819
    Story.append(
5✔
820
        KeepTogether(
821
            [
822
                doHeading(
823
                    """
824
                        Figure 1. Credentials Exposed.
825
                    """,
826
                    figure,
827
                ),
828
                get_image(BASE_DIR + "/assets/inc_date_df.png", width=6.5 * inch),
829
            ]
830
        )
831
    )
832

833
    Story.append(PageBreak())
5✔
834
    Story.append(
5✔
835
        Paragraph(
836
            """
837
            <font face="Franklin_Gothic_Medium_Regular">Table 1</font>  provides breach details. Breach descriptions can be found in Appendix A.
838
        """,
839
            body,
840
        )
841
    )
842
    Story.append(point12_spacer)
5✔
843
    Story.append(
5✔
844
        doHeading(
845
            """
846
                    Table 1. Breach Details.
847
                """,
848
            table,
849
        )
850
    )
851

852
    # add link to appendix to breach names
853
    data_dict["breach_table"]["Breach Name"] = (
5✔
854
        '<link href="#'
855
        + data_dict["breach_table"]["Breach Name"].apply(sha_hash)
856
        + '" color="#003e67">'
857
        + data_dict["breach_table"]["Breach Name"].astype(str)
858
        + "</link>"
859
    )
860
    Story.append(
5✔
861
        format_table(
862
            data_dict["breach_table"],
863
            table_header,
864
            [2.5 * inch, inch, inch, inch, inch],
865
            [body, None, None, None, None],
866
        )
867
    )
868

869
    Story.append(point12_spacer)
5✔
870
    Story.append(PageBreak())
5✔
871

872
    # ***Start Generating Domain Masquerading Page***#
873
    Story.append(
5✔
874
        KeepTogether(
875
            [
876
                doHeading("3.2 Domain Alerts and Suspected Masquerading", h2),
877
                Paragraph(
878
                    """Spoofed or typo-squatting domains can be used to host fake web pages for malicious purposes,
879
            such as imitating landing pages for spear phishing campaigns. Below are alerts of domains that appear
880
            to mimic a stakeholder's actual domain.
881
            """,
882
                    body,
883
                ),
884
                point12_spacer,
885
            ]
886
        )
887
    )
888

889
    row = [
5✔
890
        build_kpi(
891
            Paragraph(
892
                str(data_dict["domain_alerts"])
893
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Domain Alert(s)</font>""",
894
                style=kpi,
895
            ),
896
            2,
897
        ),
898
        build_kpi(
899
            Paragraph(
900
                str(data_dict["suspectedDomains"])
901
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Suspected Domain(s)</font>""",
902
                style=kpi,
903
            ),
904
            2,
905
        ),
906
    ]
907

908
    Story.append(
5✔
909
        BalancedColumns(
910
            row,  # the flowables we are balancing
911
            nCols=2,  # the number of columns
912
            needed=55,  # the minimum space needed by the flowable
913
            spaceBefore=0,
914
            spaceAfter=12,
915
            showBoundary=False,  # optional boundary showing
916
            leftPadding=65,  # these override the created frame
917
            rightPadding=0,  # paddings if specified else the
918
            topPadding=None,  # default frame paddings
919
            bottomPadding=None,  # are used
920
            innerPadding=35,  # the gap between frames if specified else
921
            # use max(leftPadding,rightPadding)
922
            name="domain_masq_kpis",  # for identification purposes when stuff goes awry
923
            endSlack=0.1,  # height disparity allowance ie 10% of available height
924
        )
925
    )
926

927
    Story.append(Paragraph("3.2.1 Domain Monitoring Alerts", h3))
5✔
928
    Story.append(
5✔
929
        Paragraph(
930
            """
931
            <font face="Franklin_Gothic_Medium_Regular">Table 2</font> shows alerts of newly registered or updated
932
            domains that appear to mimic a stakeholder's actual domain.
933
        """,
934
            body,
935
        )
936
    )
937
    Story.append(point12_spacer)
5✔
938
    Story.append(
5✔
939
        doHeading(
940
            """
941
                    Table 2. Domain Monitoring Alerts Results.
942
                """,
943
            table,
944
        )
945
    )
946
    Story.append(
5✔
947
        format_table(
948
            data_dict["domain_alerts_table"],
949
            table_header,
950
            [5.5 * inch, 1 * inch],
951
            [body, None],
952
        )
953
    )
954

955
    Story.append(point12_spacer)
5✔
956
    Story.append(
5✔
957
        KeepTogether(
958
            [
959
                Paragraph("3.2.2 Suspected Domain Masquerading", h3),
960
                Paragraph(
961
                    """
962
                    <font face="Franklin_Gothic_Medium_Regular">Table 3</font> shows registered or updated domains that were
963
                    flagged by a blocklist service.
964
                """,
965
                    body,
966
                ),
967
                point12_spacer,
968
                doHeading(
969
                    """
970
                    Table 3. Suspected Domain Masquerading Results.
971
                """,
972
                    table,
973
                ),
974
            ]
975
        )
976
    )
977

978
    Story.append(
5✔
979
        format_table(
980
            data_dict["domain_table"],
981
            table_header,
982
            [1.5 * inch, 1.5 * inch, 3.5 * inch / 3, 3.5 * inch / 3, 3.5 * inch / 3],
983
            [body, body, body, body, body],
984
        )
985
    )
986
    Story.append(point12_spacer)
5✔
987

988
    Story.append(PageBreak())
5✔
989

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

1049
    Story.append(Paragraph("3.3.1 Insecure Ports, Protocols, and Services", h3))
5✔
1050
    Story.append(
5✔
1051
        Paragraph(
1052
            """
1053
            Insecure protocols are those protocols which lack proper encryption allowing threat actors to access
1054
            data that is being transmitted and even to potentially, to control systems.
1055
            <font face="Franklin_Gothic_Medium_Regular">Figure 2</font> and
1056
            <font face="Franklin_Gothic_Medium_Regular">Table 4</font> provide detailed information for the Remote
1057
            Desktop Protocol (RDP), Server Message Block (SMB) protocol, and the Telnet application protocol.
1058
        """,
1059
            body,
1060
        )
1061
    )
1062
    Story.append(point12_spacer)
5✔
1063
    Story.append(
5✔
1064
        KeepTogether(
1065
            [
1066
                doHeading(
1067
                    """
1068
                        Figure 2. Insecure Protocols.
1069
                    """,
1070
                    figure,
1071
                ),
1072
                get_image(BASE_DIR + "/assets/pro_count.png", width=6.5 * inch),
1073
            ]
1074
        )
1075
    )
1076
    Story.append(
5✔
1077
        doHeading(
1078
            """
1079
                Table 4. Insecure Protocols.
1080
            """,
1081
            table,
1082
        )
1083
    )
1084
    Story.append(
5✔
1085
        format_table(
1086
            data_dict["risky_assets"],
1087
            table_header,
1088
            [1.5 * inch, 3.5 * inch, 1.5 * inch],
1089
            [None, body, None],
1090
        )
1091
    )
1092

1093
    Story.append(point12_spacer)
5✔
1094
    Story.append(
5✔
1095
        KeepTogether(
1096
            [
1097
                Paragraph("3.3.2 Shodan-Verified Vulnerabilities", h3),
1098
                Paragraph(
1099
                    """
1100
                    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
1101
                    through extra checks to validate the finding. Refer to Appendix A for summary data.
1102
                """,
1103
                    body,
1104
                ),
1105
                doHeading(
1106
                    """
1107
                    Table 5. Shodan-Verified Vulnerabilities.
1108
                """,
1109
                    table,
1110
                ),
1111
            ]
1112
        )
1113
    )
1114
    # add link to appendix for CVE string
1115
    data_dict["verif_vulns"]["CVE"] = (
5✔
1116
        '<link href="#'
1117
        + data_dict["verif_vulns"]["CVE"].str.replace("-", "_")
1118
        + '" color="#003e67">'
1119
        + data_dict["verif_vulns"]["CVE"].astype(str)
1120
        + "</link>"
1121
    )
1122

1123
    Story.append(
5✔
1124
        format_table(
1125
            data_dict["verif_vulns"],
1126
            table_header,
1127
            [6.5 * inch / 3, 6.5 * inch / 3, 6.5 * inch / 3],
1128
            [body, None, None],
1129
        )
1130
    )
1131

1132
    Story.append(point12_spacer)
5✔
1133

1134
    Story.append(
5✔
1135
        KeepTogether(
1136
            [
1137
                Paragraph("3.3.3 Suspected Vulnerabilities", h3),
1138
                Paragraph(
1139
                    """
1140
                        Suspected vulnerabilities are determined by the software and version an asset is running and can be used
1141
                        to understand what vulnerabilities an asset may be exposed to.
1142
                        <font face="Franklin_Gothic_Medium_Regular">Figure 3</font> identifies suspected vulnerabilities.
1143
                    """,
1144
                    body,
1145
                ),
1146
                point12_spacer,
1147
                doHeading(
1148
                    """
1149
                        Figure 3. Suspected Vulnerabilities.
1150
                    """,
1151
                    figure,
1152
                ),
1153
                get_image(
1154
                    BASE_DIR + "/assets/unverif_vuln_count.png", width=6.5 * inch
1155
                ),
1156
            ]
1157
        )
1158
    )
1159
    Story.append(PageBreak())
5✔
1160

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

1164
    row = [
5✔
1165
        build_kpi(
1166
            Paragraph(
1167
                str(data_dict["mentions_count"])
1168
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Dark Web Mentions</font>""",
1169
                style=kpi,
1170
            ),
1171
            2,
1172
        ),
1173
        build_kpi(
1174
            Paragraph(
1175
                str(data_dict["darkWeb"])
1176
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Dark Web Alerts</font>""",
1177
                style=kpi,
1178
            ),
1179
            2,
1180
        ),
1181
    ]
1182

1183
    Story.append(
5✔
1184
        BalancedColumns(
1185
            row,  # the flowables we are balancing
1186
            nCols=2,  # the number of columns
1187
            needed=55,  # the minimum space needed by the flowable
1188
            spaceBefore=0,
1189
            spaceAfter=12,
1190
            showBoundary=False,  # optional boundary showing
1191
            leftPadding=65,  # these override the created frame
1192
            rightPadding=0,  # paddings if specified else the
1193
            topPadding=None,  # default frame paddings
1194
            bottomPadding=None,  # are used
1195
            innerPadding=35,  # the gap between frames if specified else
1196
            name="dark_web_kpis",  # for identification purposes when stuff goes awry
1197
            endSlack=0.1,  # height disparity allowance ie 10% of available height
1198
        )
1199
    )
1200

1201
    Story.append(
5✔
1202
        Paragraph(
1203
            """Stakeholders and vulnerabilities are often discussed in various ways on the Dark Web. P&E monitors this
1204
                activity, as well as the source (forums, websites, tutorials), and threat actors involved. A spike in activity can
1205
                indicate a greater likelihood of an attack, vulnerability, or data leakage. This information along with a list of the
1206
                most active CVEs on the Dark Web may assist in prioritizing remediation activities.""",
1207
            style=body,
1208
        )
1209
    )
1210

1211
    Story.append(point12_spacer)
5✔
1212

1213
    Story.append(Paragraph("3.4.1 Dark Web Mentions", h3))
5✔
1214
    Story.append(
5✔
1215
        Paragraph(
1216
            """
1217
            <font face="Franklin_Gothic_Medium_Regular">Figure 4</font> provides details on the number of mentions on the
1218
            dark web during the reporting period.
1219
        """,
1220
            body,
1221
        )
1222
    )
1223
    Story.append(point12_spacer)
5✔
1224
    Story.append(
5✔
1225
        KeepTogether(
1226
            [
1227
                doHeading(
1228
                    """
1229
                        Figure 4. Dark Web Mentions.
1230
                    """,
1231
                    figure,
1232
                ),
1233
                get_image(BASE_DIR + "/assets/web_only_df_2.png", width=6.5 * inch),
1234
            ]
1235
        )
1236
    )
1237
    sub_section = 2
5✔
1238
    table_num = 6
5✔
1239
    if soc_med_included:
5!
1240
        Story.append(
5✔
1241
            KeepTogether(
1242
                [
1243
                    Paragraph("3.4.2 Most Active Social Media Posts", h3),
1244
                    Paragraph(
1245
                        """
1246
                        This result includes a list of the most active social media posts associated with a stakeholder, and tallies
1247
                        the count of “post” or “reply” actions on sites such as Telegram, Twitter, and Github.
1248
                        <font face="Franklin_Gothic_Medium_Regular">Table 6</font> identifies the social media comments count
1249
                        by organization.
1250
                    """,
1251
                        body,
1252
                    ),
1253
                    point12_spacer,
1254
                    doHeading(
1255
                        """
1256
                        Table 6. Social Media Comments Count.
1257
                    """,
1258
                        table,
1259
                    ),
1260
                ]
1261
            )
1262
        )
1263

1264
        Story.append(
5✔
1265
            format_table(
1266
                data_dict["social_med_act"],
1267
                table_header,
1268
                [5 * inch, 1.5 * inch],
1269
                [
1270
                    body,
1271
                    None,
1272
                ],
1273
            )
1274
        )
1275

1276
        Story.append(point12_spacer)
5✔
1277
        sub_section = 3
5✔
1278
        table_num = 7
5✔
1279

1280
    Story.append(
5✔
1281
        KeepTogether(
1282
            [
1283
                Paragraph(
1284
                    "3.4." + str(sub_section) + " Most Active Dark Web Posts", h3
1285
                ),
1286
                Paragraph(
1287
                    """
1288
                    This result includes a list of the most active posts associated with a stakeholder found on the dark web,
1289
                    and includes forum sites and invite-only marketplaces. <font face="Franklin_Gothic_Medium_Regular">Table """
1290
                    + str(table_num)
1291
                    + """</font>
1292
                    identifies the dark web comments count by organization.
1293
                """,
1294
                    body,
1295
                ),
1296
                point12_spacer,
1297
                doHeading(
1298
                    "Table " + str(table_num) + ". Dark Web Comments Count.", table
1299
                ),
1300
            ]
1301
        )
1302
    )
1303
    sub_section += 1
5✔
1304
    table_num += 1
5✔
1305
    Story.append(
5✔
1306
        format_table(
1307
            data_dict["dark_web_act"],
1308
            table_header,
1309
            [5 * inch, 1.5 * inch],
1310
            [
1311
                body,
1312
                None,
1313
            ],
1314
        )
1315
    )
1316

1317
    Story.append(point12_spacer)
5✔
1318

1319
    Story.append(
5✔
1320
        KeepTogether(
1321
            [
1322
                Paragraph("3.4." + str(sub_section) + " Asset Alerts", h3),
1323
                Paragraph(
1324
                    """
1325
                    <font face="Franklin_Gothic_Medium_Regular">Table """
1326
                    + str(table_num)
1327
                    + """</font> includes discussions involving stakeholder
1328
                    assets such as domain names and IPs.
1329
                """,
1330
                    body,
1331
                ),
1332
                point12_spacer,
1333
                doHeading("Table " + str(table_num) + ". Asset Alerts.", table),
1334
            ]
1335
        )
1336
    )
1337
    sub_section += 1
5✔
1338
    table_num += 1
5✔
1339
    Story.append(
5✔
1340
        format_table(
1341
            data_dict["asset_alerts"],
1342
            table_header,
1343
            [2 * inch, 3.5 * inch, 1 * inch],
1344
            [None, body, None],
1345
        )
1346
    )
1347

1348
    Story.append(point12_spacer)
5✔
1349
    Story.append(
5✔
1350
        KeepTogether(
1351
            [
1352
                Paragraph("3.4." + str(sub_section) + " Executive Alerts", h3),
1353
                Paragraph(
1354
                    """
1355
                    <font face="Franklin_Gothic_Medium_Regular">Table """
1356
                    + str(table_num)
1357
                    + """</font> includes discussions involving stakeholder
1358
                    executives and upper management.
1359
                """,
1360
                    body,
1361
                ),
1362
                point12_spacer,
1363
                doHeading("Table " + str(table_num) + ". Executive Alerts.", table),
1364
            ]
1365
        )
1366
    )
1367
    sub_section += 1
5✔
1368
    table_num += 1
5✔
1369
    Story.append(
5✔
1370
        format_table(
1371
            data_dict["alerts_exec"],
1372
            table_header,
1373
            [2 * inch, 3.5 * inch, 1 * inch],
1374
            [None, body, None],
1375
        )
1376
    )
1377

1378
    Story.append(point12_spacer)
5✔
1379

1380
    Story.append(
5✔
1381
        KeepTogether(
1382
            [
1383
                Paragraph("3.4." + str(sub_section) + " Threat Actors", h3),
1384
                Paragraph(
1385
                    """
1386
                    A threat actor's score is based on the amount of activity that person has on the dark web, the types of
1387
                    content posted, how prominent their account is on a forum, and if there is a larger circle of connections to
1388
                    other bad actors. Threat Actors are ranked 1 to 10, with 10 being the most severe.
1389
                    <font face="Franklin_Gothic_Medium_Regular">Table """
1390
                    + str(table_num)
1391
                    + """</font>
1392
                    identifies the top actors that have mentioned stakeholder assets.
1393
                """,
1394
                    body,
1395
                ),
1396
                point12_spacer,
1397
                doHeading("Table " + str(table_num) + ". Threat Actors.", table),
1398
            ]
1399
        )
1400
    )
1401
    sub_section += 1
5✔
1402
    table_num += 1
5✔
1403
    Story.append(
5✔
1404
        format_table(
1405
            data_dict["dark_web_actors"],
1406
            table_header,
1407
            [5.5 * inch, 1 * inch],
1408
            [body, None],
1409
        )
1410
    )
1411

1412
    Story.append(point12_spacer)
5✔
1413

1414
    Story.append(
5✔
1415
        KeepTogether(
1416
            [
1417
                Paragraph(
1418
                    "3.4." + str(sub_section) + " Alerts of Potential Threats", h3
1419
                ),
1420
                Paragraph(
1421
                    """
1422
                    Threats are derived by scanning suspicious chatter on the dark web that may have terms related to
1423
                    vulnerabilities. <font face="Franklin_Gothic_Medium_Regular">Table """
1424
                    + str(table_num)
1425
                    + """</font> identifies the most
1426
                    common threats.
1427
                """,
1428
                    body,
1429
                ),
1430
                point12_spacer,
1431
                doHeading(
1432
                    "Table " + str(table_num) + ". Alerts of Potential Threats.", table
1433
                ),
1434
            ]
1435
        )
1436
    )
1437
    sub_section += 1
5✔
1438
    table_num += 1
5✔
1439
    Story.append(
5✔
1440
        format_table(
1441
            data_dict["alerts_threats"],
1442
            table_header,
1443
            [2 * inch, 3.5 * inch, 1 * inch],
1444
            [None, body, None],
1445
        )
1446
    )
1447

1448
    Story.append(point12_spacer)
5✔
1449

1450
    Story.append(
5✔
1451
        KeepTogether(
1452
            [
1453
                Paragraph("3.4." + str(sub_section) + " Most Active Sites", h3),
1454
                Paragraph(
1455
                    """
1456
                    <font face="Franklin_Gothic_Medium_Regular">Table """
1457
                    + str(table_num)
1458
                    + """</font> includes the most active discussion forums where the organization is the topic of discussion.
1459
                """,
1460
                    body,
1461
                ),
1462
                point12_spacer,
1463
                doHeading("Table " + str(table_num) + ". Most Active Sites.", table),
1464
            ]
1465
        )
1466
    )
1467
    sub_section += 1
5✔
1468
    table_num += 1
5✔
1469
    Story.append(
5✔
1470
        format_table(
1471
            data_dict["dark_web_sites"],
1472
            table_header,
1473
            [5 * inch, 1.5 * inch],
1474
            [body, None],
1475
        )
1476
    )
1477

1478
    Story.append(point12_spacer)
5✔
1479

1480
    Story.append(
5✔
1481
        KeepTogether(
1482
            [
1483
                Paragraph("3.4." + str(sub_section) + " Invite-Only Market Alerts", h3),
1484
                Paragraph(
1485
                    """
1486
                    <font face="Franklin_Gothic_Medium_Regular">Table """
1487
                    + str(table_num)
1488
                    + """</font> includes the number of alerts on each invite-only
1489
                    market where compromised credentials were offered for sale.
1490
                """,
1491
                    body,
1492
                ),
1493
                point12_spacer,
1494
                doHeading(
1495
                    "Table " + str(table_num) + ". Invite-Only Market Alerts.", table
1496
                ),
1497
            ]
1498
        )
1499
    )
1500
    sub_section += 1
5✔
1501
    table_num += 1
5✔
1502
    Story.append(
5✔
1503
        format_table(
1504
            data_dict["markets_table"],
1505
            table_header,
1506
            [4 * inch, 2.5 * inch],
1507
            [None, None],
1508
        )
1509
    )
1510

1511
    Story.append(point12_spacer)
5✔
1512
    Story.append(
5✔
1513
        KeepTogether(
1514
            [
1515
                Paragraph(
1516
                    "3.4." + str(sub_section) + " Most Active CVEs on the Dark Web", h3
1517
                ),
1518
                Paragraph(
1519
                    """
1520
                    Rated by CyberSixGill's Dynamic Vulnerability Exploit (DVE) Score, this state-of-the-art machine
1521
                    learning model automatically predicts the probability of a CVE being exploited.
1522
                    <font face="Franklin_Gothic_Medium_Regular">Table """
1523
                    + str(table_num)
1524
                    + """</font> identifies the top 10 CVEs this report period.
1525
                """,
1526
                    body,
1527
                ),
1528
                point12_spacer,
1529
                doHeading(
1530
                    "Table " + str(table_num) + ". Most Active CVEs on the Dark Web.",
1531
                    table,
1532
                ),
1533
            ]
1534
        )
1535
    )
1536
    sub_section += 1
5✔
1537
    table_num += 1
5✔
1538
    Story.append(
5✔
1539
        format_table(
1540
            data_dict["top_cves"],
1541
            table_header,
1542
            [1.5 * inch, 3.5 * inch, 1.5 * inch],
1543
            [
1544
                None,
1545
                body,
1546
                None,
1547
            ],
1548
        )
1549
    )
1550

1551
    Story.append(point12_spacer)
5✔
1552

1553
    Story.append(NextPageTemplate("ContentPage"))
5✔
1554
    Story.append(PageBreak())
5✔
1555

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

1600
    Story.append(doHeading("Appendix A: Additional Information", h1))
5✔
1601
    Story.append(horizontal_line)
5✔
1602
    Story.append(point12_spacer)
5✔
1603
    # If there are breaches print breach descriptions
1604
    if len(data_dict["breach_appendix"]) > 0:
5!
NEW
1605
        Story.append(Paragraph("Credential Breach Details: ", h2))
×
NEW
1606
        Story.append(Spacer(1, 6))
×
NEW
1607
        for row in data_dict["breach_appendix"].itertuples(index=False):
×
1608
            # Add anchor points for breach links
NEW
1609
            Story.append(
×
1610
                Paragraph(
1611
                    """
1612
                <a name="{link_name}"/><font face="Franklin_Gothic_Medium_Regular">{breach_name}</font>: {description}
1613
            """.format(
1614
                        breach_name=row[0],
1615
                        description=row[1].replace(' rel="noopener"', ""),
1616
                        link_name=sha256(str(row[0]).encode("utf8")).hexdigest(),
1617
                    ),
1618
                    body,
1619
                )
1620
            )
NEW
1621
            Story.append(point12_spacer)
×
NEW
1622
        Story.append(point12_spacer)
×
1623

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

NEW
1628
        Story.append(
×
1629
            Paragraph(
1630
                """Verified vulnerabilities are determined by the Shodan scanner and identify assets with active, known vulnerabilities. More information
1631
                about CVEs can be found <link href="https://nvd.nist.gov/">here</link>.""",
1632
                body,
1633
            )
1634
        )
NEW
1635
        Story.append(point12_spacer)
×
NEW
1636
        Story.append(
×
1637
            doHeading(
1638
                "Table " + str(table_num) + ". Verified Vulnerabilities Summaries.",
1639
                table,
1640
            )
1641
        )
1642
        # Add anchor points for vuln links
NEW
1643
        data_dict["verif_vulns_summary"]["CVE"] = (
×
1644
            '<a name="'
1645
            + data_dict["verif_vulns_summary"]["CVE"].str.replace("-", "_")
1646
            + '"/>'
1647
            + data_dict["verif_vulns_summary"]["CVE"].astype(str)
1648
        )
NEW
1649
        Story.append(
×
1650
            format_table(
1651
                data_dict["verif_vulns_summary"],
1652
                table_header,
1653
                [1.5 * inch, 1.25 * inch, 0.75 * inch, 3 * inch],
1654
                [body, None, None, body],
1655
            )
1656
        )
NEW
1657
        Story.append(point12_spacer)
×
1658

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

1692
    Story.append(
5✔
1693
        Paragraph(
1694
            """
1695
            <font face="Franklin_Gothic_Medium_Regular">Suspected Domain Masquerading:</font><br/>
1696
            Spoofed or typo-squatting domains can be used to host fake web pages for malicious purposes, such as
1697
            imitating landing pages for spear phishing campaigns. This report shows newly registered or reactivated
1698
            domains that appear to mimic a stakeholder's actual domain.""",
1699
            indented,
1700
        )
1701
    )
1702

1703
    Story.append(
5✔
1704
        Paragraph(
1705
            """
1706
            <font face="Franklin_Gothic_Medium_Regular">Credentials Leaked/Exposed:</font><br/>
1707
            Credential leakage occurs when user credentials, often including passwords, are stolen via phishing campaigns,
1708
            network compromise, or misconfiguration of databases leading to public exposure. This leaked data is then listed
1709
            for sale on numerous forums and sites on the dark web, which provides attackers easy access to a stakeholders'
1710
            networks.
1711
        """,
1712
            indented,
1713
        )
1714
    )
1715

1716
    Story.append(
5✔
1717
        Paragraph(
1718
            """
1719
            <font face="Franklin_Gothic_Medium_Regular">Insecure Devices & Suspected Vulnerabilities:</font><br/>
1720
            When looking at Open-Source information gathered from tools that search the web for Internet of Things
1721
            (IoT) devices and other external facing assets. It can then be inferred that certain systems, ports, and
1722
            protocols associated with these assets are likely to have vulnerabilities, based on the OS or application
1723
            version information reported when queried. When possible, our analysis also reports on potential malware
1724
            infections for stakeholders.
1725
        """,
1726
            indented,
1727
        )
1728
    )
1729

1730
    Story.append(
5✔
1731
        KeepTogether(
1732
            Paragraph(
1733
                """
1734
                    <font face="Franklin_Gothic_Medium_Regular">Increased Dark Web Activity:</font><br/>
1735
                    Stakeholders and vulnerabilities are often discussed in various ways on the dark web. P&E monitors this
1736
                    activity, as well as the source (forums, websites, tutorials), and threat actors involved. A spike in
1737
                    activity can indicate a greater likelihood of an attack, vulnerability, or data leakage. Additionally,
1738
                    the urgency of the threat can be evaluated based on the threat actors involved along with other thresholds.
1739
                    Evaluating this content may also indicate if a stakeholder has been involved in a hacking incident as that data
1740
                    will often be published or offered 'for sale'. This information along with a list of the most active CVEs on the
1741
                    Dark Web may assist in prioritizing remediation activities.
1742

1743
                """,
1744
                indented,
1745
            )
1746
        )
1747
    )
1748

1749
    Story.append(
5✔
1750
        Paragraph(
1751
            """<font face="Franklin_Gothic_Medium_Regular">Do you perform scans of our networks?</font><br/>
1752
            P&E does not perform active scanning. The information we gather is through passive collection from numerous
1753
            public and vendor data sources. As such, we collect data on a continual basis, and provide summary reports
1754
            twice a month.
1755
        """,
1756
            body,
1757
        )
1758
    )
1759
    Story.append(point12_spacer)
5✔
1760

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

1768
        """,
1769
            body,
1770
        )
1771
    )
1772
    Story.append(point12_spacer)
5✔
1773

1774
    Story.append(
5✔
1775
        Paragraph(
1776
            """<font face="Franklin_Gothic_Medium_Regular">How will the results be provided to me?</font><br/>
1777
            P&E will provide twice monthly P&E reports as password-protected attachments to emails from
1778
            vulnerability@cisa.dhs.gov. The attachments will contain a PDF—providing a summary of the findings,
1779
            tables, graphs, as charts—as well as a JSON file containing the raw data used to generate the PDF
1780
            report to facilitate your agencies own analysis.
1781
        """,
1782
            body,
1783
        )
1784
    )
1785
    Story.append(point12_spacer)
5✔
1786

1787
    Story.append(
5✔
1788
        Paragraph(
1789
            """<font face="Franklin_Gothic_Medium_Regular">Do you offer ad-hoc analysis of source data?</font><br/>
1790
            If you have any questions about a particular vulnerability that you believe you have mitigated, but
1791
            which continues to show up in the reports, we can perform a detailed analysis to determine why your
1792
            organization continues to show that vulnerability. In many cases, the issue can be tracked back to
1793
            the fact that the mitigation has made it impossible for the reconnaissance service or tool to identify
1794
            the configuration, and as such they may default to displaying the last collected information.
1795
        """,
1796
            body,
1797
        )
1798
    )
1799
    Story.append(point12_spacer)
5✔
1800

1801
    Story.append(
5✔
1802
        Paragraph(
1803
            """<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/>
1804
            The general notification process is the same as all of the CyHy components. Simply send an email to
1805
            vulnerability@cisa.dhs.gov identifying the requested changes. In this instance, make sure to identify
1806
            “P&E Report Delivery” in the subject to ensure the issue is routed to our team.
1807
        """,
1808
            body,
1809
        )
1810
    )
1811
    Story.append(point12_spacer)
5✔
1812
    Story.append(
5✔
1813
        KeepTogether(
1814
            [
1815
                doHeading("Appendix C: Acronyms", h1),
1816
                horizontal_line,
1817
                point12_spacer,
1818
                Table(
1819
                    [
1820
                        ["CISA", "Cybersecurity and Infrastructure Security Agency"],
1821
                        ["CVE", "Common Vulnerabilities and Exposures"],
1822
                        ["DHS", "Department of Homeland Security"],
1823
                        ["DVE", "Dynamic Vulnerability Exploit"],
1824
                        ["FTP", "File Transfer Protocol"],
1825
                        ["HTTP", "Hypertext Transfer Protocol"],
1826
                        ["IP", "Internet Protocol"],
1827
                        ["P&E", "Posture and Exposure"],
1828
                        ["RDP", "Remote Desktop Protocol"],
1829
                        ["SIP", "Session Initiation Protocol"],
1830
                        ["SMB", "Server Message Block"],
1831
                    ]
1832
                ),
1833
            ]
1834
        )
1835
    )
1836
    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