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

cisagov / pe-reports / 5880348905

16 Aug 2023 02:40PM UTC coverage: 33.736% (+7.0%) from 26.737%
5880348905

Pull #565

github

DJensen94
Merge branch 'develop' into DJ_Reportlab_generator_PR
Merge changes in develop branch into reportlab branch
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%)

17 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
            # use max(leftPadding,rightPadding)
1045
            name="vulns_kpis",  # for identification purposes when stuff goes awry
1046
            endSlack=0.1,  # height disparity allowance ie 10% of available height
1047
        )
1048
    )
1049

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

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

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

1133
    Story.append(point12_spacer)
5✔
1134

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

1163
    # ***Start Generating Dark Web Page***#
1164
    Story.append(KeepTogether([doHeading("3.4 Dark Web Activity", h2), Spacer(1, 6)]))
5✔
1165

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

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

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

1214
    Story.append(point12_spacer)
5✔
1215

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

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

1279
        Story.append(point12_spacer)
5✔
1280
        sub_section = 3
5✔
1281
        table_num = 7
5✔
1282

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

1320
    Story.append(point12_spacer)
5✔
1321

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

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

1381
    Story.append(point12_spacer)
5✔
1382

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

1415
    Story.append(point12_spacer)
5✔
1416

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

1451
    Story.append(point12_spacer)
5✔
1452

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

1481
    Story.append(point12_spacer)
5✔
1482

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

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

1554
    Story.append(point12_spacer)
5✔
1555

1556
    Story.append(NextPageTemplate("ContentPage"))
5✔
1557
    Story.append(PageBreak())
5✔
1558

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

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

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

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

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

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

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

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

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

1746
                """,
1747
                indented,
1748
            )
1749
        )
1750
    )
1751

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

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

1771
        """,
1772
            body,
1773
        )
1774
    )
1775
    Story.append(point12_spacer)
5✔
1776

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

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

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