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

cisagov / gophish-tools / 4632624010

pending completion
4632624010

push

github

GitHub
Merge pull request #123 from cisagov/lineage/skeleton

141 of 473 branches covered (29.81%)

Branch coverage included in aggregate %.

9 of 24 new or added lines in 10 files covered. (37.5%)

223 existing lines in 5 files now uncovered.

298 of 1270 relevant lines covered (23.46%)

1.41 hits per line

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

10.05
/src/assessment/builder.py
1
"""Create an assessment JSON file.
2

3
Usage:
4
  pca-wizard [--log-level=LEVEL] ASSESSMENT_ID
5
  pca-wizard (-h | --help)
6
  pca-wizard --version
7

8
Options:
9
  ASSESSMENT_ID             ID of the assessment to create a JSON file for.
10
  -h --help                 Show this message.
11
  --version                 Show version.
12
  -l --log-level=LEVEL      If specified, then the log level will be set to
13
                            the specified value.  Valid values are "debug", "info",
14
                            "warning", "error", and "critical". [default: info]
15
"""
16
# Standard Python Libraries
17
import copy
6✔
18
import csv
6✔
19
from datetime import datetime
6✔
20
import json
6✔
21
import logging
6✔
22
import sys
6✔
23
from typing import Dict
6✔
24

25
# Third-Party Libraries
26
from docopt import docopt
6✔
27
from prompt_toolkit import prompt
6✔
28
from prompt_toolkit.completion import WordCompleter
6✔
29
from prompt_toolkit.shortcuts import message_dialog, radiolist_dialog
6✔
30
import pytz
6✔
31

32
# cisagov Libraries
33
from models.models import SMTP, Assessment, Campaign, Group, Page, Target, Template
6✔
34
from util.input import get_input, get_number, get_time_input, yes_no_prompt
6✔
35
from util.set_date import set_date
6✔
36
from util.validate import (
6✔
37
    BlankInputValidator,
38
    EmailValidator,
39
    MissingKey,
40
    email_import_validation,
41
    validate_domain,
42
    validate_email,
43
)
44

45
from ._version import __version__
6✔
46

47
AUTO_FORWARD = """
6✔
48
                <html>
49
                    <body onload=\"document.forms[\'auto_forward\'].submit()\">
50
                        <form action=\"\" method=\"POST\" name=\"auto_forward\"> </form>
51
                </html>
52
               """
53

54
CONFIRMATION_PROMPT = "\nDo you need to modify any of the values for this campaign?"
6✔
55

56

57
def set_time_zone():
6✔
58
    """Select a timezone from a list of US-based time zones.
59

60
    Returns: Time zone name based on pytz.
61
    """
62
    # TODO Allow for a select more option to get access to full list of Time Zones
63
    # See issue: https://github.com/cisagov/gophish-tools/issues/49
64

65
    # Creates list of US Time Zones
66
    time_zone = list()
6✔
67
    for zone in pytz.common_timezones:
6✔
68
        if zone.startswith("US/"):
6✔
69
            time_zone.append((zone, zone))
6✔
70

71
    # Ask user to select time zone from list.
72
    return radiolist_dialog(
6✔
73
        values=time_zone, title="Time Zone", text="Please select assessment time zone:"
74
    ).run()
75

76

77
def display_list_groups(assessment):
6✔
78
    """List groups in an assessment."""
79
    print("\tID\tName")
×
80
    print("\t-- \t-----")
×
81

82
    # Prints groups or No Groups message
83
    if assessment.groups:
×
84
        for index, temp_group in enumerate(assessment.groups):
×
85
            print("\t{}\t{}".format(index + 1, temp_group.name))
×
86
    else:
87
        print("\t--NO GROUPS--")
×
88

89
    print("\n")
×
90

91

92
def display_list_pages(assessment):
6✔
93
    """List pages in an assessment."""
94
    print("\tID\tName")
×
95
    print("\t-- \t-----")
×
96

97
    # Prints pages or No pages message
98
    if assessment.pages:
×
99
        for index, temp_page in enumerate(assessment.pages):
×
100
            print("\t{}\t{}".format(index + 1, temp_page.name))
×
101
    else:
102
        print("\t--NO PAGES--")
×
103

104
    print("\n")
×
105

106

107
def build_assessment(assessment_id):
6✔
108
    """Walk user through building a new assessment document.
109

110
    Returns: an assessment object
111
    """
112
    logging.info("Building Assessment")
×
113
    # Initializes assessment object with ID and timezone
114
    assessment = Assessment(id=assessment_id, timezone=set_time_zone())
×
115

116
    # Uses prompt to set Assessment and target domains while not allowing blank input
117
    assessment.domain = get_input("    Assessment Domain (subdomain.domain.tld):")
×
118
    assessment.target_domains = (
×
119
        get_input("    Targeted domain(s) separated by spaces:").lower().split(" ")
120
    )
121

122
    # Uses functions to build out aspects of assessment.
123
    assessment.pages = build_pages(assessment.id)
×
124
    assessment.groups = build_groups(assessment.id, assessment.target_domains)
×
125

126
    template_smtp = SMTP()
×
127
    template_smtp.name = assessment.id + "-SP"
×
128

129
    # Sets up smtp host info to be pre-populated.
130
    template_smtp.host = prompt(
×
131
        "Enter SMTP Host: ", default=template_smtp.host, validator=BlankInputValidator()
132
    )
133

134
    # Bandit complains about the input() function, but it is safe to
135
    # use in Python 3, which is required by this project.
136
    template_smtp.username = input("SMTP User: ")  # nosec
×
137
    template_smtp.password = input("SMTP Password: ")  # nosec
×
138

139
    assessment.campaigns = list()
×
140
    logging.info("Building Campaigns")
×
141
    num_campaigns = get_number("    How many Campaigns?")
×
142
    for campaign_number in range(0, num_campaigns):
×
143
        campaign_data = build_campaigns(assessment, campaign_number + 1, template_smtp)
×
144
        assessment.campaigns.append(campaign_data)
×
145

146
        set_date("start_date", assessment, campaign_data.launch_date)
×
147
        set_date("end_date", assessment, campaign_data.complete_date)
×
148

149
    return assessment
×
150

151

152
def build_campaigns(assessment, campaign_number, template_smtp):
6✔
153
    """Build a campaign."""
154
    # Set up component holders
155
    logging.info("Building Campaign %s", assessment.id)
×
156
    campaign = Campaign(name=assessment.id)
×
157
    campaign_tz = pytz.timezone(assessment.timezone)
×
158

159
    # Get Launch Time
160
    while True:
161
        campaign.launch_date = get_time_input("start", assessment.timezone)
×
162

163
        if campaign.launch_date > datetime.now(campaign_tz).isoformat():
×
164
            break
×
165
        else:
166
            logging.error("Launch date is not after the current datetime")
×
167

168
    while True:
169
        campaign.complete_date = get_time_input("end", assessment.timezone)
×
170

171
        if campaign.complete_date > campaign.launch_date:
×
172
            pass  # Do nothing yet and continue checks
×
173
        else:
174
            logging.error("Complete date is not after launch date.")
×
175

176
        if campaign.complete_date > datetime.now(campaign_tz).isoformat():
×
177
            break  # Valid input, break out of loop
×
178
        else:
179
            logging.error("Complete date is not after the current datetime.")
×
180

181
    campaign.smtp, campaign.template = import_email(
×
182
        assessment, campaign_number, template_smtp
183
    )
184

185
    # Select Group:
186
    campaign.group_name = select_group(assessment)
×
187

188
    # Select page:
189
    campaign.page_name = select_page(assessment)
×
190

191
    campaign.url = prompt(
×
192
        "    Campaign URL: ",
193
        default="http://" + assessment.domain,
194
        validator=BlankInputValidator(),
195
    )
196

197
    campaign = review_campaign(campaign)
×
198

199
    logging.info("Successfully Added Campaign {}".format(campaign.name))
×
200

201
    return campaign
×
202

203

204
def review_campaign(campaign):
6✔
205
    """Review a campaign."""
206
    # TODO Review group name and page name.
207
    campaign_dict = campaign.as_dict()
×
208
    while True:
209
        print("\n")
×
210

211
        # Outputs relevent fields except Email Template.
212
        for field, value in campaign_dict.items():
×
213
            if field in [
×
214
                "launch_date",
215
                "complete_date",
216
                "url",
217
                "name",
218
                "group_name",
219
                "page_name",
220
            ]:
221
                print("{}: {}".format(field.replace("_", " ").title(), value))
×
222
            elif field == "smtp":
×
223
                print("SMTP: ")
×
224
                for smtp_key, smtp_value in campaign_dict["smtp"].items():
×
225
                    print(
×
226
                        "\t{}: {}".format(
227
                            smtp_key.replace("_", " ").title(), smtp_value
228
                        )
229
                    )
230

231
        if yes_no_prompt(CONFIRMATION_PROMPT) == "yes":
×
232
            completer = WordCompleter(
×
233
                campaign_dict.keys().remove("template"), ignore_case=True
234
            )
235
            # Loops to get valid Field name form user.
236
            while True:
237
                update_key = prompt(
×
238
                    "Which Field: ",
239
                    completer=completer,
240
                    validator=BlankInputValidator(),
241
                ).lower()
242
                if update_key != "smtp":
×
243
                    try:
×
244
                        update_value = prompt(
×
245
                            "{}: ".format(update_key),
246
                            default=campaign_dict[update_key],
247
                            validator=BlankInputValidator(),
248
                        )
249
                    except KeyError:
×
250
                        logging.error("Incorrect Field!")
×
251
                    else:
252
                        setattr(campaign, update_key, update_value)
×
253
                        break
×
254
                else:
255
                    # Builds a word completion list with each word of the option being capitalized.
256
                    sub_completer = WordCompleter(
×
257
                        list(
258
                            map(
259
                                lambda sub_field: sub_field.replace("_", " ").title(),
260
                                campaign_dict[update_key].as_dict().keys(),
261
                            )
262
                        ),
263
                        ignore_case=True,
264
                    )
265
                    update_sub = prompt(
×
266
                        "Which Indicator: ",
267
                        completer=sub_completer,
268
                        validator=BlankInputValidator(),
269
                    ).lower()
270
                    try:
×
271
                        update_value = prompt(
×
272
                            "{}: ".format(update_sub),
273
                            default=campaign_dict[update_key].as_dict()[update_sub],
274
                            validator=BlankInputValidator(),
275
                        )
276
                    except KeyError:
×
277
                        logging.error("Incorrect Field!")
×
278
                    else:
279
                        setattr(campaign_dict[update_key], update_sub, update_value)
×
280
                        break
×
281
        else:
282
            break
×
283

284
    return campaign
×
285

286

287
def select_group(assessment):
6✔
288
    """Select a group."""
289
    # Select Group:
290
    if len(assessment.groups) == 1:  # If only one auto sets.
×
291
        logging.info("Group auto set to {}".format(assessment.groups[0].name))
×
292
        group_name = assessment.groups[0].name
×
293
    else:  # Allows user to choose from multiple groups;
294
        while True:
295
            try:
×
296
                display_list_groups(assessment)
×
297
                group_name = assessment.groups[
×
298
                    get_number("    Select Group for this Campaign?") - 1
299
                ].name
300
                break
×
301
            except IndexError:
×
302
                logging.error("ERROR: Invalid selection, try again.")
×
303

304
    return group_name
×
305

306

307
def select_page(assessment):
6✔
308
    """Select a page."""
309
    if len(assessment.pages) == 1:  # If only one auto sets.
×
310
        logging.info("Page auto set to {}".format(assessment.pages[0].name))
×
311
        page_name = assessment.pages[0].name
×
312
    else:  # Allows user to choose from multiple pages
313
        while True:
314
            try:
×
315
                print("\n")
×
316
                display_list_pages(assessment)
×
317
                page_name = assessment.pages[
×
318
                    get_number("    Select the Page for this Campaign?") - 1
319
                ].name
320
                break
×
321
            except IndexError:
×
322
                logging.error("ERROR: Invalid selection, try again.")
×
323

324
    return page_name
×
325

326

327
def import_email(assessment, campaign_number, template_smtp):
6✔
328
    """Import email from file."""
329
    temp_template = Template(name=f"{assessment.id}-T{str(campaign_number)}")
×
330
    temp_smtp = copy.deepcopy(template_smtp)
×
331
    temp_smtp.name = f"{assessment.id}-SP-{campaign_number}"
×
332

333
    # Receives the file name and checks if it exists.
334
    while True:
335
        try:
×
336
            import_file_name = get_input("    Import File name?")
×
337
            # Drops .json if included so it can always be added as fail safe.
338
            import_file_name = import_file_name.split(".", 1)[0]
×
339

340
            with open(import_file_name + ".json") as importFile:
×
341
                import_temp = json.load(importFile)
×
342

343
            # Validates that all fields are present or raise MissingKey Error.
344
            email_import_validation(import_temp)
×
345
            break
×
NEW
346
        except OSError:
×
347
            logging.critical("Import File not found: {}.json".format(import_file_name))
×
348
            print("Please try again...")
×
349

350
        except MissingKey as e:
×
351
            # Logs and indicates the user should correct before clicking ok which will re-run the import.
352
            logging.critical("Missing Field from import: {}".format(e.key))
×
353
            message_dialog(
×
354
                title="Missing Field",
355
                text=f'Email import is missing the "{e.key}" field, please correct before clicking Ok.\n {e.key}: {e.description}',
356
            )
357

358
            continue
×
359

360
    # Finalize SMTP profile, push to Gophish for check.
361
    # TODO Need to valid this formatting.
362
    temp_smtp.from_address = import_temp["from_address"]
×
363

364
    # Load
365
    temp_template.subject = import_temp["subject"]
×
366
    temp_template.html = import_temp["html"]
×
367
    temp_template.text = import_temp["text"]
×
368
    temp_template.name = f"{assessment.id}-T{str(campaign_number)}-{import_temp['id']}"
×
369

370
    return temp_smtp, temp_template
×
371

372

373
def create_email(assessment, campaign_number=""):
6✔
374
    """Create email."""
375
    temp_template = Template(name=assessment.id + "-T" + str(campaign_number))
×
376
    temp_smtp = SMTP(name=f"{assessment.id}-SP-{campaign_number}")
×
377

378
    # Receives the file name and checks if it exists.
379
    while True:
380
        try:
×
381
            html_file_name = get_input("HTML Template File name:")
×
382
            # Drops .html if included so it can always be added as fail safe.
383
            html_file_name = html_file_name.split(".", 1)[0]
×
384

385
            with open(html_file_name + ".html") as htmlFile:
×
386
                temp_template.html = htmlFile.read()
×
387

388
            break
×
NEW
389
        except OSError:
×
390
            logging.error(f"HTML Template File not found: {html_file_name}.html")
×
391
            print("Please try again...")
×
392

393
        # Receives the file name and checks if it exists.
394
    while True:
395
        try:
×
396
            text_file_name = get_input("    Text Template File name:")
×
397
            # Drops .txt if included so it can always be added as fail safe.
398
            text_file_name = text_file_name.split(".", 1)[0]
×
399

400
            with open(text_file_name + ".txt") as textFile:
×
401
                temp_template.text = textFile.read()
×
402

403
            break
×
NEW
404
        except OSError:
×
405
            logging.critical(
×
406
                "Text Template File not found: {}.txt".format(text_file_name)
407
            )
408
            print("Please try again...")
×
409

410
    return temp_smtp, temp_template
×
411

412

413
def build_groups(id, target_domains):
6✔
414
    """Build groups."""
415
    logging.info("Getting Group Metadata")
×
416
    groups = list()
×
417

418
    # Looks through to get the number of groups as a number with error checking
419
    num_groups = get_number("    How many groups do you need?")
×
420

421
    if num_groups > 1:
×
422
        logging.warning("NOTE: Please load each group as a different CSV")
×
423

424
    labels = yes_no_prompt("    Are there customer labels?")
×
425

426
    for group_num in range(int(num_groups)):
×
427
        logging.info(f"Building Group {group_num + 1}")
×
428

429
        new_group = Group(name=f"{id}-G{str(group_num + 1)}")
×
430

431
        new_group.targets = build_emails(target_domains, labels)
×
432

433
        logging.info(f"Group Ready: {new_group.name}")
×
434
        groups.append(new_group)
×
435

436
    return groups
×
437

438

439
def build_emails(domains, labels):
6✔
440
    """Build emails."""
441
    # Holds list of Users to be added to group.
442
    targets = list()
×
443
    domain_miss_match = list()
×
444
    format_error = list()
×
445

446
    # Receives the file name and checks if it exists.
447
    while True:
448
        try:
×
449
            email_file_name = get_input("    E-mail CSV name:")
×
450
            # Drops .csv if included so it can always be added as fail safe.
451
            email_file_name = email_file_name.split(".", 1)[0]
×
452

453
            with open(email_file_name + ".csv") as csv_file:
×
454
                read_csv = csv.reader(csv_file, delimiter=",")
×
455
                next(read_csv)
×
456

457
                for row in read_csv:
×
458
                    # Checks e-mail format, if false prints message.
459
                    if not validate_email(row[2]):
×
460
                        format_error.append(row)
×
461
                    # Checks that the domain matches, if false prints message,
462
                    elif not validate_domain(row[2], domains):
×
463
                        domain_miss_match.append(row)
×
464

465
                    else:
466
                        target = Target(
×
467
                            first_name=row[0], last_name=row[1], email=row[2]
468
                        )
469
                        target = target_add_label(labels, row, target)
×
470
                        targets.append(target)
×
471

472
                # Works through emails found to include formatting errors.
473
                print("\n")
×
474
                if len(format_error) < 2:
×
475
                    for email in format_error:
×
476
                        email[2] = prompt(
×
477
                            "Correct Email Formatting: ",
478
                            default=email[2],
479
                            validator=EmailValidator(),
480
                        )
481
                        if not validate_domain(email[2], domains):
×
482
                            domain_miss_match.append(email)
×
483
                        else:
484
                            target = Target(
×
485
                                first_name=email[0], last_name=email[1], email=email[2]
486
                            )
487
                            target = target_add_label(labels, email, target)
×
488
                            targets.append(target)
×
489
                else:
490
                    logging.error("{} Formatting Errors".format(len(format_error)))
×
491
                    if (
×
492
                        yes_no_prompt("Would you like to correct each here? (yes/no)")
493
                        == "yes"
494
                    ):
495
                        for email in format_error:
×
496
                            email[2] = prompt(
×
497
                                "Correct Email Formatting: ",
498
                                default=email[2],
499
                                validator=EmailValidator(),
500
                            )
501
                            if not validate_domain(email[2], domains):
×
502
                                domain_miss_match.append(email)
×
503
                            else:
504
                                target = Target(
×
505
                                    first_name=row[0], last_name=row[1], email=row[2]
506
                                )
507
                                target = target_add_label(labels, email, target)
×
508
                                targets.append(target)
×
509
                    else:
510
                        logging.warning(
×
511
                            "Incorrectly formatted Emails will not be added, continuing..."
512
                        )
513

514
                # Works through emails found to have domain miss match.
515
                if len(domain_miss_match) < 2:
×
516
                    for email in domain_miss_match:
×
517
                        email[2] = prompt(
×
518
                            "Correct Email Domain: ",
519
                            default=email[2],
520
                            validator=EmailValidator(),
521
                        )
522
                else:
523
                    logging.error("{} Domain Mismatch Errors".format(len(format_error)))
×
524
                    if (
×
525
                        yes_no_prompt("Would you like to correct each here? (yes/no)")
526
                        == "yes"
527
                    ):
528
                        for email in domain_miss_match:
×
529
                            while True:
530
                                email[2] = prompt(
×
531
                                    "Correct Email Domain: ",
532
                                    default=email[2],
533
                                    validator=EmailValidator(),
534
                                )
535
                                if validate_domain(email[2], domains):
×
536
                                    target = Target(
×
537
                                        first_name=row[0],
538
                                        last_name=row[1],
539
                                        email=row[2],
540
                                    )
541
                                    target = target_add_label(labels, email, target)
×
542
                                    targets.append(target)
×
543
                                    break
×
544
                    else:
545
                        logging.warning(
×
546
                            "Incorrectly formatted Emails will not be added, continuing..."
547
                        )
548

549
            if len(targets) == 0:
×
550
                raise Exception("No targets loaded")
×
551
            break
×
NEW
552
        except OSError:
×
553
            logging.critical("Email File not found: {}.csv".format(email_file_name))
×
554
            print("\t Please try again...")
×
555
        except Exception:
×
556
            # Logs and indicates the user should correct before clicking ok which will re-run the import.
557
            logging.critical("No targets loaded")
×
558
            message_dialog(
×
559
                title="Missing Targets",
560
                text="No targets loaded from file, please check file before clicking Ok.",
561
            )
562
            continue
×
563

564
    return targets
×
565

566

567
def target_add_label(labels, email, target):
6✔
568
    """Add a label to a target."""
569
    if labels == "yes" and not email[3]:
×
570
        logging.error("Missing Label for {}".format(target.email))
×
571
        target.position = get_input("Please enter a label:")
×
572
    else:
573
        target.position = email[3]
×
574
    return target
×
575

576

577
def build_pages(id_):
6✔
578
    """Walk user through building multiple new page documents.
579

580
    :return a page object
581
    """
582
    pages = list()
×
583
    logging.info("Getting Page Metadata")
×
584

585
    # Looks through to get the number of pages as a number with error checking
586
    num_pages = get_number("    How many pages do you need?")
×
587

588
    for page_num in range(int(num_pages)):
×
589
        logging.info(f"Building Page {page_num + 1}")
×
590
        temp_page = Page()
×
591
        auto_forward = yes_no_prompt("    Will this page auto forward?")
×
592
        temp_page.capture_credentials = False
×
593
        temp_page.capture_passwords = False
×
594

595
        if auto_forward == "yes":
×
596
            setattr(temp_page, "name", f"{id_}-{page_num+1}-AutoForward")
×
597
            temp_page.html = AUTO_FORWARD
×
598
            temp_page.redirect_url = get_input("    URL to Redirect to:")
×
599

600
        else:
601
            temp_page.name = f"{id_}-{page_num+1}-Landing"
×
602
            forward = yes_no_prompt("    Will this page forward after action? (yes/no)")
×
603
            if forward == "yes":
×
604
                temp_page.redirect_url = get_input("    URL to Redirect to:")
×
605

606
            # Receives the file name and checks if it exists.
607
            while True:
608
                try:
×
609
                    landing_file_name = get_input("Landing Page File name:")
×
610
                    # Drops .html if included so it can always be added as fail safe.
611
                    landing_file_name = landing_file_name.split(".", 1)[0]
×
612

613
                    with open(landing_file_name + ".html") as landingFile:
×
614
                        temp_page.html = landingFile.read()
×
615

616
                    break
×
NEW
617
                except OSError:
×
618
                    logging.critical(
×
619
                        f"ERROR- Landing Page File not found: {landing_file_name}.html"
620
                    )
621
                    print("Please try again...")
×
622

623
        # Debug page information
624
        logging.debug(f"Page Name: {temp_page.name}")
×
625
        logging.debug(f"Redirect ULR: {temp_page.redirect_url}")
×
626
        logging.debug(f"Capture Credentials: {temp_page.capture_credentials}")
×
627
        logging.debug(f"Capture Passwords: {temp_page.capture_passwords}")
×
628

629
        temp_page = review_page(temp_page)
×
630
        pages.append(temp_page)
×
631

632
    return pages
×
633

634

635
def review_page(page):
6✔
636
    """Review page."""
637
    # Loops until not changes are required.
638
    while True:
639
        print("\n")
×
640
        page_keys = list()
×
641
        for key, value in page.as_dict().items():
×
642
            if key != "html":
×
643
                print("{}: {}".format(key, value))
×
644
                page_keys.append(key)
×
645
        if yes_no_prompt(CONFIRMATION_PROMPT) == "yes":
×
646
            completer = WordCompleter(page_keys, ignore_case=True)
×
647

648
            # Loops to get valid Field name form user.
649
            while True:
650
                update_key = prompt(
×
651
                    "Which Field: ",
652
                    completer=completer,
653
                    validator=BlankInputValidator(),
654
                ).lower()
655

656
                try:
×
657
                    update_value = prompt(
×
658
                        "{}: ".format(update_key),
659
                        default=page.as_dict()[update_key],
660
                        validator=BlankInputValidator(),
661
                    )
662
                except KeyError:
×
663
                    logging.error("Incorrect Field!")
×
664
                else:
665
                    setattr(page, update_key, update_value)
×
666
                    break
×
667
        else:
668
            break
×
669
    return page
×
670

671

672
def main() -> None:
6✔
673
    """Set up logging and call the build_assessments function."""
674
    args: Dict[str, str] = docopt(__doc__, version=__version__)
×
675

676
    # Set up logging
677
    log_level = args["--log-level"]
×
678
    try:
×
679
        logging.basicConfig(
×
680
            format="\n%(levelname)s: %(message)s", level=log_level.upper()
681
        )
682
    except ValueError:
×
683
        logging.critical(
×
684
            '"{}"is not a valid logging level.  Possible values are debug, info, warning, and error.'.format(
685
                log_level
686
            )
687
        )
688
        sys.exit(1)
×
689

690
    assessment = build_assessment(args["ASSESSMENT_ID"])
×
691

692
    with open(f"{assessment.id}.json", "w") as fp:
×
693
        json.dump(assessment.as_dict(), fp, indent=4)
×
694

695
    logging.info(f"Assessment JSON ready: {assessment.id}.json")
×
696
    # Stop logging and clean up
697
    logging.shutdown()
×
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