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

cisagov / pe-reports / 5939885323

22 Aug 2023 02:02PM UTC coverage: 33.736% (+7.0%) from 26.737%
5939885323

Pull #565

github

web-flow
replace underscores with dashes

replace underscores with dashes in docopt parameters

Co-authored-by: Shane Frasier <jeremy.frasier@gwe.cisa.dhs.gov>
Pull Request #565: Update report generator to use reportlab

93 of 477 branches covered (19.5%)

Branch coverage included in aggregate %.

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

18 existing lines in 5 files now uncovered.

801 of 2173 relevant lines covered (36.86%)

1.83 hits per line

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

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

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

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

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

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

52

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

57

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

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

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

71

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

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

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

84

85
class MyDocTemplate(BaseDocTemplate):
5✔
86
    """Customize the document template."""
87

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

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

120

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

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

129

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

137

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

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

160
    data = header_row + data
5✔
161

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

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

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

213

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

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

242

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1111
    Story.append(point12_spacer)
5✔
1112

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

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

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

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

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

1190
    Story.append(point12_spacer)
5✔
1191

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

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

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

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

1296
    Story.append(point12_spacer)
5✔
1297

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

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

1357
    Story.append(point12_spacer)
5✔
1358

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

1391
    Story.append(point12_spacer)
5✔
1392

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

1427
    Story.append(point12_spacer)
5✔
1428

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

1457
    Story.append(point12_spacer)
5✔
1458

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

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

1530
    Story.append(point12_spacer)
5✔
1531

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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