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

cisagov / pe-reports / 5892227966

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

Pull #565

github

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

93 of 477 branches covered (19.5%)

Branch coverage included in aggregate %.

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

18 existing lines in 5 files now uncovered.

801 of 2173 relevant lines covered (36.86%)

1.83 hits per line

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

12.22
/src/pe_reports/reportlab_core_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

40
pdfmetrics.registerFont(
5✔
41
    TTFont("Franklin_Gothic_Book", BASE_DIR + "/fonts/FranklinGothicBook.ttf")
42
)
43

44

45
pdfmetrics.registerFont(
5✔
46
    TTFont(
47
        "Franklin_Gothic_Medium_Regular",
48
        BASE_DIR + "/fonts/FranklinGothicMediumRegular.ttf",
49
    )
50
)
51

52
defaultPageSize = letter
5✔
53
PAGE_HEIGHT = defaultPageSize[1]
5✔
54
PAGE_WIDTH = defaultPageSize[0]
5✔
55

56

57
def sha_hash(s: str):
5✔
58
    """Hash a given string."""
NEW
59
    return sha256(s.encode("utf-8")).hexdigest()
×
60

61

62
# Extend Table of contents to create a List of Figures Class
63
class ListOfFigures(TableOfContents):
5✔
64
    """Class extention to build a Table of Figures."""
65

66
    def notify(self, kind, stuff):
5✔
67
        """
68
        Call he notification hook to register all kinds of events.
69

70
        Here we are interested in 'Figure' events only.
71
        """
NEW
72
        if kind == "TOCFigure":
×
NEW
73
            self.addEntry(*stuff)
×
74

75

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

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

83
        Here we are interested in 'Table' events only.
84
        """
NEW
85
        if kind == "TOCTable":
×
NEW
86
            self.addEntry(*stuff)
×
87

88

89
class MyDocTemplate(BaseDocTemplate):
5✔
90
    """Extend the BaseDocTemplate to adjust Template."""
91

92
    def __init__(self, filename, **kw):
5✔
93
        """Initialize MyDocTemplate."""
NEW
94
        self.allowSplitting = 0
×
NEW
95
        BaseDocTemplate.__init__(self, filename, **kw)
×
NEW
96
        self.pagesize = defaultPageSize
×
97

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

124

125
class ConditionalSpacer(Spacer):
5✔
126
    """Create a Conditional Spacer class."""
127

128
    def wrap(self, availWidth, availHeight):
5✔
129
        """Create a spacer if there is space on the page to do so."""
NEW
130
        height = min(self.height, availHeight - 1e-8)
×
NEW
131
        return (availWidth, height)
×
132

133

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

141

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

NEW
160
            current_row.append(cell)
×
NEW
161
            current_cell += 1
×
NEW
162
        data.append(current_row)
×
163

NEW
164
    data = header_row + data
×
165

NEW
166
    table = Table(
×
167
        data,
168
        colWidths=column_widths,
169
        rowHeights=None,
170
        style=None,
171
        splitByRow=1,
172
        repeatRows=1,
173
        repeatCols=0,
174
        rowSplitRange=(2, -1),
175
        spaceBefore=None,
176
        spaceAfter=None,
177
        cornerRadii=None,
178
    )
179

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

NEW
200
    if len(df) == 0:
×
NEW
201
        label = Paragraph(
×
202
            "No Data to Report",
203
            ParagraphStyle(
204
                name="centered",
205
                fontName="Franklin_Gothic_Medium_Regular",
206
                textColor=HexColor("#a7a7a6"),
207
                fontSize=16,
208
                leading=16,
209
                alignment=1,
210
                spaceAfter=10,
211
                spaceBefore=10,
212
            ),
213
        )
NEW
214
        table = KeepTogether([table, label])
×
NEW
215
    return table
×
216

217

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

NEW
234
    style = TableStyle(
×
235
        [
236
            ("VALIGN", (0, 0), (-1, 0), "MIDDLE"),
237
            ("ALIGN", (0, 0), (-1, -1), "CENTER"),
238
            ("VALIGN", (0, 1), (-1, -1), "MIDDLE"),
239
            ("GRID", (0, 0), (0, 0), 1, HexColor("#003e67")),
240
            ("BACKGROUND", (0, 0), (0, 0), HexColor("#DEEBF7")),
241
        ]
242
    )
NEW
243
    table.setStyle(style)
×
NEW
244
    return table
×
245

246

247
def core_report_gen(data_dict):
5✔
248
    """Generate a P&E report with data passed in the data dictionry."""
249

NEW
250
    def titlePage(canvas, doc):
×
251
        """Build static elements of the cover page."""
NEW
252
        canvas.saveState()
×
NEW
253
        canvas.drawImage(BASE_DIR + "/assets/Cover.png", 0, 0, width=None, height=None)
×
NEW
254
        canvas.setFont("Franklin_Gothic_Medium_Regular", 32)
×
NEW
255
        canvas.drawString(50, 660, "Posture & Exposure Report")
×
NEW
256
        canvas.restoreState()
×
257

NEW
258
    def summaryPage(canvas, doc):
×
259
        """Build static elements of the summary page."""
NEW
260
        canvas.saveState()
×
NEW
261
        canvas.setFont("Franklin_Gothic_Book", 13)
×
NEW
262
        canvas.drawImage(
×
263
            BASE_DIR + "/assets/core-summary-background.png",
264
            0,
265
            0,
266
            width=PAGE_WIDTH,
267
            height=PAGE_HEIGHT,
268
        )
NEW
269
        canvas.setFillColor(HexColor("#1d5288"))
×
NEW
270
        canvas.setStrokeColor("#1d5288")
×
NEW
271
        canvas.rect(inch, 210 + 1 * inch, 3.5 * inch, 4.7 * inch, fill=1)
×
NEW
272
        canvas.restoreState()
×
NEW
273
        summary_frame = Frame(
×
274
            1.1 * inch, 224, 3.3 * inch, 5.5 * inch, id=None, showBoundary=0
275
        )
NEW
276
        summary_1_style = ParagraphStyle(
×
277
            "summary_1_style",
278
            fontSize=12,
279
            alignment=0,
280
            textColor="white",
281
            fontName="Franklin_Gothic_Book",
282
        )
NEW
283
        summary_1 = Paragraph(
×
284
            """
285
        <font face="Franklin_Gothic_Medium_Regular">Credential Publication & Abuse:</font><br/>
286
        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.
287
        <br/><br/><br/><br/>
288
        <font face="Franklin_Gothic_Medium_Regular">Suspected Domain Masquerading Attempt:</font><br/>
289
        Registered domain names that are similar to legitimate domains which attempt to trick users into navigating to illegitimate domains.
290
        <br/><br/><br/><br/><br/><br/>
291
        <font face="Franklin_Gothic_Medium_Regular">Insecure Devices & Vulnerabilities:</font><br/>
292
        Open ports, risky protocols, insecure products, and externally observable vulnerabilities are potential targets for exploit.
293
        """,
294
            style=summary_1_style,
295
        )
NEW
296
        summary_frame.addFromList([summary_1], canvas)
×
297

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

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

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

NEW
328
        json_title_frame = Frame(
×
329
            3.85 * inch, 175, 1.5 * inch, 0.5 * inch, id=None, showBoundary=0
330
        )
NEW
331
        json_title = Paragraph(
×
332
            "JSON&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;EXCEL",
333
            style=json_excel,
334
        )
NEW
335
        json_title_frame.addFromList([json_title], canvas)
×
336

NEW
337
        canvas.setStrokeColor("#a7a7a6")
×
NEW
338
        canvas.setFillColor("#a7a7a6")
×
NEW
339
        canvas.drawInlineImage(
×
340
            BASE_DIR + "/assets/cisa.png", 45, 705, width=65, height=65
341
        )
NEW
342
        canvas.drawString(130, 745, "Posture and Exposure Report")
×
NEW
343
        canvas.drawString(130, 725, "Reporting Period: " + data_dict["dateRange"])
×
NEW
344
        canvas.line(130, 710, PAGE_WIDTH - inch, 710)
×
NEW
345
        canvas.drawRightString(
×
346
            PAGE_WIDTH - inch, 0.75 * inch, "P&E Report | Page %d" % (doc.page)
347
        )
NEW
348
        canvas.drawString(inch, 0.75 * inch, data_dict["endDate"])
×
NEW
349
        canvas.setFont("Franklin_Gothic_Medium_Regular", 12)
×
NEW
350
        canvas.setFillColor("#FFC000")
×
NEW
351
        canvas.drawString(6.4 * inch, 745, "TLP: AMBER")
×
352

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

NEW
372
    def doHeading(text, sty):
×
373
        """Add a bookmark to heading element to allow linking from the table of contents."""
374
        # create bookmarkname
NEW
375
        bn = sha256((text + sty.name).encode("utf8")).hexdigest()
×
376
        # modify paragraph text to include an anchor point with name bn
NEW
377
        h = Paragraph(text + '<a name="%s"/>' % bn, sty)
×
378
        # store the bookmark name on the flowable so afterFlowable can see this
NEW
379
        h._bookmarkName = bn
×
NEW
380
        return h
×
381

382
    # ***Document Structures***#
383
    """Build frames for different page structures."""
NEW
384
    doc = MyDocTemplate(data_dict["filename"])
×
NEW
385
    title_frame = Frame(45, 390, 530, 250, id=None, showBoundary=0)
×
NEW
386
    frameT = Frame(
×
387
        doc.leftMargin,
388
        doc.bottomMargin,
389
        PAGE_WIDTH - (2 * inch),
390
        PAGE_HEIGHT - (2.4 * inch),
391
        id="normal",
392
        showBoundary=0,
393
    )
NEW
394
    doc.addPageTemplates(
×
395
        [
396
            PageTemplate(id="TitlePage", frames=title_frame, onPage=titlePage),
397
            PageTemplate(id="SummaryPage", frames=frameT, onPage=summaryPage),
398
            PageTemplate(id="ContentPage", frames=frameT, onPage=contentPage),
399
        ]
400
    )
NEW
401
    Story = []
×
402
    """Build table of contents."""
NEW
403
    toc = TableOfContents()
×
NEW
404
    tof = ListOfFigures()
×
NEW
405
    tot = ListOfTables()
×
406

407
    """Create font and formatting styles."""
NEW
408
    PS = ParagraphStyle
×
409

NEW
410
    centered = PS(
×
411
        name="centered",
412
        fontName="Franklin_Gothic_Medium_Regular",
413
        fontSize=20,
414
        leading=16,
415
        alignment=1,
416
        spaceAfter=10,
417
        spaceBefore=10,
418
    )
419

NEW
420
    indented = PS(
×
421
        name="indented",
422
        fontName="Franklin_Gothic_Book",
423
        fontSize=12,
424
        leading=14,
425
        leftIndent=30,
426
        spaceAfter=20,
427
    )
428

NEW
429
    h1 = PS(
×
430
        fontName="Franklin_Gothic_Medium_Regular",
431
        name="Heading1",
432
        fontSize=16,
433
        leading=18,
434
        textColor=HexColor("#003e67"),
435
    )
436

NEW
437
    h2 = PS(
×
438
        name="Heading2",
439
        fontName="Franklin_Gothic_Medium_Regular",
440
        fontSize=14,
441
        leading=10,
442
        textColor=HexColor("#003e67"),
443
        spaceAfter=12,
444
    )
445

NEW
446
    h3 = PS(
×
447
        name="Heading3",
448
        fontName="Franklin_Gothic_Medium_Regular",
449
        fontSize=14,
450
        leading=10,
451
        textColor=HexColor("#003e67"),
452
        spaceAfter=10,
453
    )
454

NEW
455
    body = PS(
×
456
        name="body",
457
        leading=14,
458
        fontName="Franklin_Gothic_Book",
459
        fontSize=12,
460
    )
461

NEW
462
    kpi = PS(
×
463
        name="kpi",
464
        fontName="Franklin_Gothic_Medium_Regular",
465
        fontSize=14,
466
        leading=16,
467
        alignment=1,
468
        spaceAfter=20,
469
    )
470

NEW
471
    json_excel = PS(
×
472
        name="json_excel",
473
        fontName="Franklin_Gothic_Medium_Regular",
474
        fontSize=10,
475
        alignment=1,
476
    )
477

NEW
478
    figure = PS(
×
479
        name="figure",
480
        fontName="Franklin_Gothic_Medium_Regular",
481
        fontSize=12,
482
        leading=16,
483
        alignment=1,
484
    )
485

NEW
486
    table = PS(
×
487
        name="table",
488
        fontName="Franklin_Gothic_Medium_Regular",
489
        fontSize=12,
490
        leading=16,
491
        alignment=1,
492
        spaceAfter=12,
493
    )
494

NEW
495
    table_header = PS(
×
496
        name="table_header",
497
        fontName="Franklin_Gothic_Medium_Regular",
498
        fontSize=12,
499
        leading=16,
500
        alignment=1,
501
        spaceAfter=12,
502
        textColor=HexColor("#FFFFFF"),
503
    )
504

NEW
505
    title_data = PS(
×
506
        fontName="Franklin_Gothic_Medium_Regular", name="Title", fontSize=18, leading=20
507
    )
508

509
    """Stream all the dynamic content to the report."""
510

511
    # *************************#
512
    # Create repeated elements
NEW
513
    point12_spacer = ConditionalSpacer(1, 12)
×
NEW
514
    horizontal_line = HRFlowable(
×
515
        width="100%",
516
        thickness=1.5,
517
        lineCap="round",
518
        color=HexColor("#003e67"),
519
        spaceBefore=0,
520
        spaceAfter=1,
521
        hAlign="LEFT",
522
        vAlign="TOP",
523
        dash=None,
524
    )
525
    # ***Title Page***#
NEW
526
    Story.append(Paragraph("Prepared for: " + data_dict["department"], title_data))
×
NEW
527
    Story.append(point12_spacer)
×
NEW
528
    Story.append(Paragraph("Reporting Period: " + data_dict["dateRange"], title_data))
×
NEW
529
    Story.append(NextPageTemplate("ContentPage"))
×
NEW
530
    Story.append(PageBreak())
×
531

532
    # ***Table of Contents***#
NEW
533
    Story.append(Paragraph("<b>Table of Contents</b>", centered))
×
534
    # Set styles for levels in Table of contents
NEW
535
    toc_styles = [
×
536
        PS(
537
            fontName="Franklin_Gothic_Medium_Regular",
538
            fontSize=14,
539
            name="TOCHeading1",
540
            leftIndent=20,
541
            firstLineIndent=-20,
542
            spaceBefore=1,
543
            leading=14,
544
        ),
545
        PS(
546
            fontSize=12,
547
            name="TOCHeading2",
548
            leftIndent=40,
549
            firstLineIndent=-20,
550
            spaceBefore=0,
551
            leading=12,
552
        ),
553
        PS(
554
            fontSize=10,
555
            name="TOCHeading3",
556
            leftIndent=60,
557
            firstLineIndent=-20,
558
            spaceBefore=0,
559
            leading=12,
560
        ),
561
        PS(
562
            fontSize=10,
563
            name="TOCHeading4",
564
            leftIndent=100,
565
            firstLineIndent=-20,
566
            spaceBefore=0,
567
            leading=12,
568
        ),
569
    ]
NEW
570
    toc.levelStyles = toc_styles
×
NEW
571
    Story.append(toc)
×
NEW
572
    Story.append(PageBreak())
×
573

574
    # ***Table of Figures and Table of Contents***#
NEW
575
    tot.levelStyles = toc_styles
×
NEW
576
    tof.levelStyles = toc_styles
×
NEW
577
    Story.append(Paragraph("<b>Table of Figures</b>", centered))
×
NEW
578
    Story.append(tof)
×
NEW
579
    Story.append(Paragraph("<b>Table of Tables</b>", centered))
×
NEW
580
    Story.append(tot)
×
NEW
581
    Story.append(PageBreak())
×
582

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

602
    # dark web analysis, romoved this from line 606
NEW
603
    Story.append(
×
604
        ListFlowable(
605
            [
606
                ListItem(
607
                    Paragraph("Credentials Leaked/Exposed", body),
608
                    leftIndent=35,
609
                    value="bulletchar",
610
                ),
611
                ListItem(
612
                    Paragraph("Domain Masquerading and Monitoring", body),
613
                    leftIndent=35,
614
                    value="bulletchar",
615
                ),
616
                ListItem(
617
                    Paragraph("Vulnerabilities & Malware Associations", body),
618
                    leftIndent=35,
619
                    value="bulletchar",
620
                ),
621
                ListItem(
622
                    Paragraph("Hidden Assets and Risky Services", body),
623
                    leftIndent=35,
624
                    value="bulletchar",
625
                ),
626
            ],
627
            bulletType="bullet",
628
            start="bulletchar",
629
            leftIndent=10,
630
        )
631
    )
632

NEW
633
    Story.append(
×
634
        Paragraph(
635
            """<br/>It is important to note that these findings have not been verified; everything is
636
                            gathered via passive analysis of publicly available sources. As such there may be false
637
                            positive findings; however, these findings should be treated as information that your
638
                            organization is leaking out to the internet for adversaries to notice.<br/><br/>""",
639
            body,
640
        )
641
    )
642

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

NEW
696
    Story.append(doHeading("1.3 Contact Information", h2))
×
NEW
697
    Story.append(
×
698
        Paragraph("Posture and Exposure Team Email: vulnerability@cisa.dhs.gov", body)
699
    )
700

NEW
701
    Story.append(NextPageTemplate("SummaryPage"))
×
NEW
702
    Story.append(PageBreak())
×
703

704
    # ***Start Generating Summary Page***#
NEW
705
    Story.append(doHeading("2. Summary of Findings", h1))
×
NEW
706
    Story.append(horizontal_line)
×
NEW
707
    Story.append(point12_spacer)
×
NEW
708
    Story.append(doHeading("2.1 Summary of Tracked Data", h2))
×
NEW
709
    Story.append(Spacer(1, 425))
×
NEW
710
    Story.append(doHeading("2.2 Raw Data Links", h2))
×
NEW
711
    Story.append(
×
712
        Paragraph(
713
            "Exposed Credentials<br/><br/>Domain Masquerading and Monitoring<br/><br/>Vulnerabilities and Malware Associations",
714
            body,
715
        )
716
    )
717

NEW
718
    Story.append(NextPageTemplate("ContentPage"))
×
NEW
719
    Story.append(PageBreak())
×
720

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

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

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

NEW
807
    Story.append(PageBreak())
×
NEW
808
    Story.append(
×
809
        Paragraph(
810
            """
811
            <font face="Franklin_Gothic_Medium_Regular">Table 1</font>  provides breach details. Breach descriptions can be found in Appendix A.
812
        """,
813
            body,
814
        )
815
    )
NEW
816
    Story.append(point12_spacer)
×
NEW
817
    Story.append(
×
818
        doHeading(
819
            """
820
                    Table 1. Breach Details.
821
                """,
822
            table,
823
        )
824
    )
825

826
    # add link to appendix to breach names
NEW
827
    data_dict["breach_table"]["Breach Name"] = (
×
828
        '<link href="#'
829
        + data_dict["breach_table"]["Breach Name"].apply(sha_hash)
830
        + '" color="#003e67">'
831
        + data_dict["breach_table"]["Breach Name"].astype(str)
832
        + "</link>"
833
    )
NEW
834
    Story.append(
×
835
        format_table(
836
            data_dict["breach_table"],
837
            table_header,
838
            [2.5 * inch, inch, inch, inch, inch],
839
            [body, None, None, None, None],
840
        )
841
    )
842

NEW
843
    Story.append(point12_spacer)
×
NEW
844
    Story.append(PageBreak())
×
845

846
    # ***Start Generating Domain Masquerading Page***#
NEW
847
    Story.append(
×
848
        KeepTogether(
849
            [
850
                doHeading("3.2 Domain Alerts and Suspected Masquerading", h2),
851
                Paragraph(
852
                    """Spoofed or typo-squatting domains can be used to host fake web pages for malicious purposes,
853
            such as imitating landing pages for spear phishing campaigns. Below are alerts of domains that appear
854
            to mimic a stakeholder's actual domain.
855
            """,
856
                    body,
857
                ),
858
                point12_spacer,
859
            ]
860
        )
861
    )
862

NEW
863
    row = [
×
864
        build_kpi(
865
            Paragraph(
866
                str(data_dict["domain_alerts"])
867
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Domain Alert(s)</font>""",
868
                style=kpi,
869
            ),
870
            2,
871
        ),
872
        build_kpi(
873
            Paragraph(
874
                str(data_dict["suspectedDomains"])
875
                + """<br/> <font face="Franklin_Gothic_Book" size='10'>Suspected Domain(s)</font>""",
876
                style=kpi,
877
            ),
878
            2,
879
        ),
880
    ]
881

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

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

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

NEW
952
    Story.append(
×
953
        format_table(
954
            data_dict["domain_table"],
955
            table_header,
956
            [1.5 * inch, 1.5 * inch, 3.5 * inch / 3, 3.5 * inch / 3, 3.5 * inch / 3],
957
            [body, body, body, body, body],
958
        )
959
    )
NEW
960
    Story.append(point12_spacer)
×
961

NEW
962
    Story.append(PageBreak())
×
963

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

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

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

NEW
1098
    Story.append(
×
1099
        format_table(
1100
            data_dict["verif_vulns"],
1101
            table_header,
1102
            [6.5 * inch / 3, 6.5 * inch / 3, 6.5 * inch / 3],
1103
            [body, None, None],
1104
        )
1105
    )
1106

NEW
1107
    Story.append(point12_spacer)
×
1108

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

1138
    # ***Start Generating Methodology Page***#
NEW
1139
    Story.append(doHeading("4. Methodology", h1))
×
NEW
1140
    Story.append(horizontal_line)
×
NEW
1141
    Story.append(point12_spacer)
×
NEW
1142
    Story.append(doHeading("4.1 Background", h2))
×
NEW
1143
    Story.append(
×
1144
        Paragraph(
1145
            """Cyber Hygiene's Posture and Exposure is a service provided by the Cybersecurity
1146
            and Infrastructure Security Agency (CISA).<br/><br/>
1147
            Cyber Hygiene started providing Posture and Exposure reports in October 2020 to assess,
1148
            on a recurring basis, the security posture of your organization by tracking dark web activity,
1149
            domain alerts, vulnerabilites, and credential exposures.""",
1150
            body,
1151
        )
1152
    )
NEW
1153
    Story.append(point12_spacer)
×
NEW
1154
    Story.append(doHeading("4.2 Process", h2))
×
NEW
1155
    Story.append(
×
1156
        Paragraph(
1157
            """Upon submission of an Acceptance Letter, DHS provided CISA with their
1158
            public network address information.<br/><br/>
1159
            The Posture and Exposure team uses this information to conduct investigations
1160
            with various open-source tools. Resulting data is then parsed for key-findings
1161
            and alerts. Summary data and detailed overviews are organized into this report
1162
            and packaged into an encrypted file for delivery.""",
1163
            body,
1164
        )
1165
    )
NEW
1166
    Story.append(point12_spacer)
×
NEW
1167
    Story.append(doHeading("5. Conclusion", h1))
×
NEW
1168
    Story.append(horizontal_line)
×
NEW
1169
    Story.append(point12_spacer)
×
NEW
1170
    Story.append(
×
1171
        Paragraph(
1172
            """Your organization should use the data provided in this report to correct any identified vulnerabilities,
1173
            exposures, or posture concerns. If you have any questions, comments, or concerns about the findings or data
1174
            contained in this report, please work with your designated technical point of contact when requesting
1175
            assistance from CISA at vulnerability@cisa.dhs.gov.""",
1176
            body,
1177
        )
1178
    )
NEW
1179
    Story.append(NextPageTemplate("ContentPage"))
×
NEW
1180
    Story.append(PageBreak())
×
1181

NEW
1182
    Story.append(doHeading("Appendix A: Additional Information", h1))
×
NEW
1183
    Story.append(horizontal_line)
×
NEW
1184
    Story.append(point12_spacer)
×
NEW
1185
    Story.append(
×
1186
        KeepTogether(
1187
            [
1188
                doHeading(" Most Active CVEs on the Dark Web", h2),
1189
                Paragraph(
1190
                    """
1191
                    <font face="Franklin_Gothic_Medium_Regular">Table 6</font> ranks the top 10 CVEs based on their
1192
                    criticality and potential for exploitation. These vulnerabilities have been carefully evaluated
1193
                    to assess their risk levels and the likelihood of being targeted by malicious actors.
1194

1195
                """,
1196
                    body,
1197
                ),
1198
                point12_spacer,
1199
                doHeading(
1200
                    "Table 6. Most Active CVEs.",
1201
                    table,
1202
                ),
1203
            ]
1204
        )
1205
    )
1206

NEW
1207
    Story.append(
×
1208
        format_table(
1209
            data_dict["top_cves"],
1210
            table_header,
1211
            [1.5 * inch, 3.5 * inch, 1.5 * inch],
1212
            [
1213
                None,
1214
                body,
1215
                None,
1216
            ],
1217
        )
1218
    )
NEW
1219
    Story.append(point12_spacer)
×
1220

1221
    # If there are breaches print breach descriptions
NEW
1222
    if len(data_dict["breach_appendix"]) > 0:
×
NEW
1223
        Story.append(doHeading("Credential Breach Details: ", h2))
×
NEW
1224
        Story.append(Spacer(1, 6))
×
NEW
1225
        for row in data_dict["breach_appendix"].itertuples(index=False):
×
1226
            # Add anchor points for breach links
NEW
1227
            Story.append(
×
1228
                Paragraph(
1229
                    """
1230
                <a name="{link_name}"/><font face="Franklin_Gothic_Medium_Regular">{breach_name}</font>: {description}
1231
            """.format(
1232
                        breach_name=row[0],
1233
                        description=row[1].replace(' rel="noopener"', ""),
1234
                        link_name=sha256(str(row[0]).encode("utf8")).hexdigest(),
1235
                    ),
1236
                    body,
1237
                )
1238
            )
NEW
1239
            Story.append(point12_spacer)
×
NEW
1240
        Story.append(point12_spacer)
×
1241

1242
    # If there are verified vulns print summary info table
NEW
1243
    if len(data_dict["verif_vulns_summary"]) > 0:
×
NEW
1244
        Story.append(doHeading("Verified Vulnerability Summaries:", h2))
×
1245

NEW
1246
        Story.append(
×
1247
            Paragraph(
1248
                """Verified vulnerabilities are determined by the Shodan scanner and identify assets with active, known vulnerabilities. More information
1249
                about CVEs can be found <link href="https://nvd.nist.gov/">here</link>.""",
1250
                body,
1251
            )
1252
        )
NEW
1253
        Story.append(point12_spacer)
×
NEW
1254
        Story.append(
×
1255
            doHeading(
1256
                "Table 7. Verified Vulnerabilities Summaries.",
1257
                table,
1258
            )
1259
        )
1260
        # Add anchor points for vuln links
NEW
1261
        data_dict["verif_vulns_summary"]["CVE"] = (
×
1262
            '<a name="'
1263
            + data_dict["verif_vulns_summary"]["CVE"].str.replace("-", "_")
1264
            + '"/>'
1265
            + data_dict["verif_vulns_summary"]["CVE"].astype(str)
1266
        )
NEW
1267
        Story.append(
×
1268
            format_table(
1269
                data_dict["verif_vulns_summary"],
1270
                table_header,
1271
                [1.5 * inch, 1.25 * inch, 0.75 * inch, 3 * inch],
1272
                [body, None, None, body],
1273
            )
1274
        )
NEW
1275
        Story.append(point12_spacer)
×
1276

NEW
1277
    Story.append(
×
1278
        KeepTogether(
1279
            [
1280
                doHeading("Appendix B: Frequently Asked Questions", h1),
1281
                horizontal_line,
1282
                point12_spacer,
1283
                Paragraph(
1284
                    """<font face="Franklin_Gothic_Medium_Regular">How are P&E data and reports different from other reports I receive from CISA?</font><br/>
1285
            The Cybersecurity and Infrastructure Security Agency's (CISA) Cyber Hygiene Posture and Exposure (P&E)
1286
            analysis is a cost-free service that helps stakeholders monitor and evaluate their cyber posture for
1287
            weaknesses found in public source information, which is readily available to an attacker to view.
1288
            P&E utilizes passive reconnaissance services, dark web analysis, and other public information
1289
            sources to identify suspected domain masquerading, credentials that have been leaked or exposed,
1290
            insecure devices, and suspected vulnerabilities.
1291
            """,
1292
                    body,
1293
                ),
1294
            ]
1295
        )
1296
    )
NEW
1297
    Story.append(point12_spacer)
×
NEW
1298
    Story.append(
×
1299
        Paragraph(
1300
            """<font face="Franklin_Gothic_Medium_Regular">What should I expect in terms of P&E's Findings? </font><br/>
1301
            The Posture and Exposure team uses numerous tools and open-source intelligence (OSINT) gathering tactics to
1302
            identify the potential weaknesses listed below. The data is then analyzed and complied into a Posture and
1303
            Exposure Report which provides both executive level information and detailed information for analysts that
1304
            includes the raw findings.""",
1305
            body,
1306
        )
1307
    )
NEW
1308
    Story.append(point12_spacer)
×
1309

NEW
1310
    Story.append(
×
1311
        Paragraph(
1312
            """
1313
            <font face="Franklin_Gothic_Medium_Regular">Suspected Domain Masquerading:</font><br/>
1314
            Spoofed or typo-squatting domains can be used to host fake web pages for malicious purposes, such as
1315
            imitating landing pages for spear phishing campaigns. This report shows newly registered or reactivated
1316
            domains that appear to mimic a stakeholder's actual domain.""",
1317
            indented,
1318
        )
1319
    )
1320

NEW
1321
    Story.append(
×
1322
        Paragraph(
1323
            """
1324
            <font face="Franklin_Gothic_Medium_Regular">Credentials Leaked/Exposed:</font><br/>
1325
            Credential leakage occurs when user credentials, often including passwords, are stolen via phishing campaigns,
1326
            network compromise, or misconfiguration of databases leading to public exposure. This leaked data is then listed
1327
            for sale on numerous forums and sites on the dark web, which provides attackers easy access to a stakeholders'
1328
            networks.
1329
        """,
1330
            indented,
1331
        )
1332
    )
1333

NEW
1334
    Story.append(
×
1335
        Paragraph(
1336
            """
1337
            <font face="Franklin_Gothic_Medium_Regular">Insecure Devices & Suspected Vulnerabilities:</font><br/>
1338
            When looking at Open-Source information gathered from tools that search the web for Internet of Things
1339
            (IoT) devices and other external facing assets. It can then be inferred that certain systems, ports, and
1340
            protocols associated with these assets are likely to have vulnerabilities, based on the OS or application
1341
            version information reported when queried. When possible, our analysis also reports on potential malware
1342
            infections for stakeholders.
1343
        """,
1344
            indented,
1345
        )
1346
    )
1347

NEW
1348
    Story.append(
×
1349
        Paragraph(
1350
            """<font face="Franklin_Gothic_Medium_Regular">Do you perform scans of our networks?</font><br/>
1351
            P&E does not perform active scanning. The information we gather is through passive collection from numerous
1352
            public and vendor data sources. As such, we collect data on a continual basis, and provide summary reports
1353
            twice a month.
1354
        """,
1355
            body,
1356
        )
1357
    )
NEW
1358
    Story.append(point12_spacer)
×
1359

NEW
1360
    Story.append(
×
1361
        Paragraph(
1362
            """<font face="Franklin_Gothic_Medium_Regular">Do you perform scans of our networks?</font><br/>
1363
            P&E does not perform active scanning. The information we gather is through passive collection from numerous
1364
            public and vendor data sources. As such, we collect data on a continual basis, and provide summary reports
1365
            twice a month.
1366

1367
        """,
1368
            body,
1369
        )
1370
    )
NEW
1371
    Story.append(point12_spacer)
×
1372

NEW
1373
    Story.append(
×
1374
        Paragraph(
1375
            """<font face="Franklin_Gothic_Medium_Regular">How will the results be provided to me?</font><br/>
1376
            P&E will provide twice monthly P&E reports as password-protected attachments to emails from
1377
            vulnerability@cisa.dhs.gov. The attachments will contain a PDF—providing a summary of the findings,
1378
            tables, graphs, as charts—as well as a JSON file containing the raw data used to generate the PDF
1379
            report to facilitate your agencies own analysis.
1380
        """,
1381
            body,
1382
        )
1383
    )
NEW
1384
    Story.append(point12_spacer)
×
1385

NEW
1386
    Story.append(
×
1387
        Paragraph(
1388
            """<font face="Franklin_Gothic_Medium_Regular">Do you offer ad-hoc analysis of source data?</font><br/>
1389
            If you have any questions about a particular vulnerability that you believe you have mitigated, but
1390
            which continues to show up in the reports, we can perform a detailed analysis to determine why your
1391
            organization continues to show that vulnerability. In many cases, the issue can be tracked back to
1392
            the fact that the mitigation has made it impossible for the reconnaissance service or tool to identify
1393
            the configuration, and as such they may default to displaying the last collected information.
1394
        """,
1395
            body,
1396
        )
1397
    )
NEW
1398
    Story.append(point12_spacer)
×
1399

NEW
1400
    Story.append(
×
1401
        Paragraph(
1402
            """<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/>
1403
            The general notification process is the same as all of the CyHy components. Simply send an email to
1404
            vulnerability@cisa.dhs.gov identifying the requested changes. In this instance, make sure to identify
1405
            “P&E Report Delivery” in the subject to ensure the issue is routed to our team.
1406
        """,
1407
            body,
1408
        )
1409
    )
NEW
1410
    Story.append(point12_spacer)
×
NEW
1411
    Story.append(
×
1412
        KeepTogether(
1413
            [
1414
                doHeading("Appendix C: Acronyms", h1),
1415
                horizontal_line,
1416
                point12_spacer,
1417
                Table(
1418
                    [
1419
                        ["CISA", "Cybersecurity and Infrastructure Security Agency"],
1420
                        ["CVE", "Common Vulnerabilities and Exposures"],
1421
                        ["DHS", "Department of Homeland Security"],
1422
                        ["DVE", "Dynamic Vulnerability Exploit"],
1423
                        ["FTP", "File Transfer Protocol"],
1424
                        ["HTTP", "Hypertext Transfer Protocol"],
1425
                        ["IP", "Internet Protocol"],
1426
                        ["P&E", "Posture and Exposure"],
1427
                        ["RDP", "Remote Desktop Protocol"],
1428
                        ["SIP", "Session Initiation Protocol"],
1429
                        ["SMB", "Server Message Block"],
1430
                    ]
1431
                ),
1432
            ]
1433
        )
1434
    )
NEW
1435
    doc.multiBuild(Story)
×
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