prelude-cli-beta 1446__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,881 @@
1
+ import click
2
+ import json
3
+ import requests
4
+ from time import sleep
5
+
6
+ from prelude_cli_beta.views.shared import Spinner, pretty_print
7
+ from prelude_sdk_beta.controllers.export_controller import ExportController
8
+ from prelude_sdk_beta.controllers.jobs_controller import JobsController
9
+ from prelude_sdk_beta.controllers.scm_controller import ScmController
10
+ from prelude_sdk_beta.models.codes import (
11
+ Control,
12
+ ControlCategory,
13
+ PartnerEvents,
14
+ PolicyType,
15
+ RunCode,
16
+ SCMCategory,
17
+ )
18
+
19
+
20
+ @click.group()
21
+ @click.pass_context
22
+ def scm(ctx):
23
+ """SCM system commands"""
24
+ ctx.obj = ScmController(account=ctx.obj)
25
+
26
+
27
+ @scm.command("endpoints")
28
+ @click.option(
29
+ "--limit", default=100, help="maximum number of results to return", type=int
30
+ )
31
+ @click.option("--odata_filter", help="OData filter string", default=None)
32
+ @click.option("--odata_orderby", help="OData orderby string", default=None)
33
+ @click.pass_obj
34
+ @pretty_print
35
+ def endpoints(controller, limit, odata_filter, odata_orderby):
36
+ """List endpoints with SCM data"""
37
+ with Spinner(description="Fetching endpoints from partner"):
38
+ return controller.endpoints(
39
+ filter=odata_filter, orderby=odata_orderby, top=limit
40
+ )
41
+
42
+
43
+ @scm.command("inboxes")
44
+ @click.option(
45
+ "--limit", default=100, help="maximum number of results to return", type=int
46
+ )
47
+ @click.option("--odata_filter", help="OData filter string", default=None)
48
+ @click.option("--odata_orderby", help="OData orderby string", default=None)
49
+ @click.pass_obj
50
+ @pretty_print
51
+ def inboxes(controller, limit, odata_filter, odata_orderby):
52
+ """List inboxes with SCM data"""
53
+ with Spinner(description="Fetching inboxes from partner"):
54
+ return controller.inboxes(filter=odata_filter, orderby=odata_orderby, top=limit)
55
+
56
+
57
+ @scm.command("network_devices")
58
+ @click.option(
59
+ "--limit", default=100, help="maximum number of results to return", type=int
60
+ )
61
+ @click.option("--odata_filter", help="OData filter string", default=None)
62
+ @click.option("--odata_orderby", help="OData orderby string", default=None)
63
+ @click.pass_obj
64
+ @pretty_print
65
+ def network_devices(controller, limit, odata_filter, odata_orderby):
66
+ """List network devices with SCM data"""
67
+ with Spinner(description="Fetching network devices from partner"):
68
+ return controller.network_devices(
69
+ filter=odata_filter, orderby=odata_orderby, top=limit
70
+ )
71
+
72
+
73
+ @scm.command("users")
74
+ @click.option(
75
+ "--limit", default=100, help="maximum number of results to return", type=int
76
+ )
77
+ @click.option("--odata_filter", help="OData filter string", default=None)
78
+ @click.option("--odata_orderby", help="OData orderby string", default=None)
79
+ @click.pass_obj
80
+ @pretty_print
81
+ def users(controller, limit, odata_filter, odata_orderby):
82
+ """List users with SCM data"""
83
+ with Spinner(description="Fetching users from partner"):
84
+ return controller.users(filter=odata_filter, orderby=odata_orderby, top=limit)
85
+
86
+
87
+ @scm.command("technique-summary")
88
+ @click.option(
89
+ "-q",
90
+ "--techniques",
91
+ help="comma-separated list of techniques to filter by",
92
+ type=str,
93
+ required=True,
94
+ )
95
+ @click.pass_obj
96
+ @pretty_print
97
+ def technique_summary(controller, techniques):
98
+ """Get policy summary per technique"""
99
+ with Spinner(description="Getting policy summary by technique"):
100
+ return controller.technique_summary(techniques=techniques)
101
+
102
+
103
+ @scm.command("evaluation-summary")
104
+ @click.option(
105
+ "--endpoint_odata_filter", help="OData filter string for endpoints", default=None
106
+ )
107
+ @click.option(
108
+ "--inbox_odata_filter", help="OData filter string for inboxes", default=None
109
+ )
110
+ @click.option("--user_odata_filter", help="OData filter string for users", default=None)
111
+ @click.option(
112
+ "-q",
113
+ "--techniques",
114
+ help="comma-separated list of techniques to filter by",
115
+ type=str,
116
+ default=None,
117
+ )
118
+ @click.pass_obj
119
+ @pretty_print
120
+ def evaluation_summary(
121
+ controller, endpoint_odata_filter, inbox_odata_filter, user_odata_filter, techniques
122
+ ):
123
+ """Get policy evaluation summary for all partners"""
124
+ with Spinner(description="Getting policy evaluation summary"):
125
+ return controller.evaluation_summary(
126
+ endpoint_filter=endpoint_odata_filter,
127
+ inbox_filter=inbox_odata_filter,
128
+ user_filter=user_odata_filter,
129
+ techniques=techniques,
130
+ )
131
+
132
+
133
+ @scm.command("evaluation")
134
+ @click.argument(
135
+ "partner",
136
+ type=click.Choice(
137
+ [c.name for c in Control if c != Control.INVALID], case_sensitive=False
138
+ ),
139
+ )
140
+ @click.option("--instance_id", required=True, help="instance ID of the partner")
141
+ @click.option("--odata_filter", help="OData filter string", default=None)
142
+ @click.option(
143
+ "-p",
144
+ "--policy_type",
145
+ help="Policy types to filter by",
146
+ multiple=True,
147
+ default=[],
148
+ type=click.Choice(
149
+ [p.name for p in PolicyType if p != PolicyType.INVALID], case_sensitive=False
150
+ ),
151
+ )
152
+ @click.option(
153
+ "-q",
154
+ "--techniques",
155
+ help="comma-separated list of techniques to filter by",
156
+ type=str,
157
+ default=None,
158
+ )
159
+ @click.pass_obj
160
+ @pretty_print
161
+ def evaluation(controller, partner, instance_id, odata_filter, policy_type, techniques):
162
+ """Get policy evaluation for given partner"""
163
+ with Spinner(description="Getting policy evaluation"):
164
+ return controller.evaluation(
165
+ partner=Control[partner],
166
+ instance_id=instance_id,
167
+ filter=odata_filter,
168
+ policy_types=",".join(policy_type),
169
+ techniques=techniques,
170
+ )
171
+
172
+
173
+ @scm.command("sync")
174
+ @click.argument(
175
+ "partner",
176
+ type=click.Choice(
177
+ [c.name for c in Control if c != Control.INVALID], case_sensitive=False
178
+ ),
179
+ required=True,
180
+ )
181
+ @click.option("--instance_id", required=True, help="instance ID of the partner")
182
+ @click.pass_obj
183
+ @pretty_print
184
+ def sync(controller, partner, instance_id):
185
+ """Update policy evaluation for given partner"""
186
+ with Spinner(description="Updating policy evaluation"):
187
+ job_id = controller.update_evaluation(
188
+ partner=Control[partner], instance_id=instance_id
189
+ )["job_id"]
190
+ jobs = JobsController(account=controller.account)
191
+ while (result := jobs.job_status(job_id))["end_time"] is None:
192
+ sleep(3)
193
+ return result
194
+
195
+
196
+ @scm.command("export")
197
+ @click.argument(
198
+ "type",
199
+ type=click.Choice(
200
+ [c.name for c in SCMCategory if c.value > 0], case_sensitive=False
201
+ ),
202
+ )
203
+ @click.option(
204
+ "-o",
205
+ "--output_file",
206
+ help="csv filename to export to",
207
+ type=click.Path(writable=True),
208
+ required=True,
209
+ )
210
+ @click.option(
211
+ "--limit", default=None, help="maximum number of results to return", type=int
212
+ )
213
+ @click.option("--odata_filter", help="OData filter string", default=None)
214
+ @click.option("--odata_orderby", help="OData orderby string", default=None)
215
+ @click.pass_obj
216
+ @pretty_print
217
+ def export(controller, type, output_file, limit, odata_filter, odata_orderby):
218
+ """Export SCM data"""
219
+ with Spinner(description="Exporting SCM data"):
220
+ export = ExportController(account=controller.account)
221
+ jobs = JobsController(account=controller.account)
222
+ job_id = export.export_scm(
223
+ export_type=SCMCategory[type],
224
+ filter=odata_filter,
225
+ orderby=odata_orderby,
226
+ top=limit,
227
+ )["job_id"]
228
+ while (result := jobs.job_status(job_id))["end_time"] is None:
229
+ sleep(3)
230
+ if result["successful"]:
231
+ data = requests.get(result["results"]["url"], timeout=10).content
232
+ with open(output_file, "wb") as f:
233
+ f.write(data)
234
+ return result, f"Exported data to {output_file}"
235
+
236
+
237
+ @click.group()
238
+ @click.pass_context
239
+ def group(ctx):
240
+ """SCM group commands"""
241
+ ctx.obj = ScmController(account=ctx.obj.account)
242
+
243
+
244
+ scm.add_command(group)
245
+
246
+
247
+ @group.command("list")
248
+ @click.option("--odata_filter", help="OData filter string", default=None)
249
+ @click.option("--odata_orderby", help="OData orderby string", default=None)
250
+ @click.pass_obj
251
+ @pretty_print
252
+ def list_partner_groups(controller, odata_filter, odata_orderby):
253
+ """List all partner groups"""
254
+ with Spinner(description="Fetching partner groups"):
255
+ return controller.list_partner_groups(
256
+ filter=odata_filter, orderby=odata_orderby
257
+ )
258
+
259
+
260
+ @group.command("sync")
261
+ @click.argument(
262
+ "partner",
263
+ type=click.Choice(
264
+ [c.name for c in Control if c != Control.INVALID], case_sensitive=False
265
+ ),
266
+ required=True,
267
+ )
268
+ @click.option("--instance_id", required=True, help="instance ID of the partner")
269
+ @click.option("--group_ids", required=True, help="comma-separated list of group IDs")
270
+ @click.pass_obj
271
+ @pretty_print
272
+ def sync_groups(controller, partner, instance_id, group_ids):
273
+ """Update groups for a partner"""
274
+ with Spinner(description="Updating groups"):
275
+ job_id = controller.update_partner_groups(
276
+ partner=Control[partner],
277
+ instance_id=instance_id,
278
+ group_ids=group_ids.split(","),
279
+ )["job_id"]
280
+ jobs = JobsController(account=controller.account)
281
+ while (result := jobs.job_status(job_id))["end_time"] is None:
282
+ sleep(3)
283
+ return result
284
+
285
+
286
+ @click.group()
287
+ @click.pass_context
288
+ def threat(ctx):
289
+ """SCM threat commands"""
290
+ ctx.obj = ScmController(account=ctx.obj.account)
291
+
292
+
293
+ scm.add_command(threat)
294
+
295
+
296
+ @threat.command("create")
297
+ @click.argument("name")
298
+ @click.option(
299
+ "-d", "--description", help="description of the threat", default=None, type=str
300
+ )
301
+ @click.option("--id", help="uuid for threat", default=None, type=str)
302
+ @click.option(
303
+ "-g", "--generated", help="was the threat AI generated", default=False, type=bool
304
+ )
305
+ @click.option(
306
+ "-p", "--published", help="date the threat was published", default=None, type=str
307
+ )
308
+ @click.option(
309
+ "-s", "--source", help="source of threat (ex. www.cisa.gov)", default=None, type=str
310
+ )
311
+ @click.option(
312
+ "-i",
313
+ "--source_id",
314
+ help="ID of the threat, per the source (ex. aa23-075a)",
315
+ default=None,
316
+ type=str,
317
+ )
318
+ @click.option(
319
+ "-q",
320
+ "--techniques",
321
+ help="comma-separated list of techniques (MITRE ATT&CK IDs)",
322
+ default=None,
323
+ type=str,
324
+ )
325
+ @click.pass_obj
326
+ @pretty_print
327
+ def create_threat(
328
+ controller,
329
+ name,
330
+ description,
331
+ id,
332
+ generated,
333
+ published,
334
+ source,
335
+ source_id,
336
+ techniques,
337
+ ):
338
+ """Create an scm threat"""
339
+ with Spinner(description="Creating scm threat"):
340
+ return controller.create_threat(
341
+ name=name,
342
+ description=description,
343
+ id=id,
344
+ generated=generated,
345
+ published=published,
346
+ source=source,
347
+ source_id=source_id,
348
+ techniques=techniques,
349
+ )
350
+
351
+
352
+ @threat.command("delete")
353
+ @click.argument("threat_id")
354
+ @click.confirmation_option(prompt="Are you sure?")
355
+ @click.pass_obj
356
+ @pretty_print
357
+ def delete_threat(controller, threat_id):
358
+ """Delete an scm threat"""
359
+ with Spinner(description="Removing scm threat"):
360
+ return controller.delete_threat(id=threat_id)
361
+
362
+
363
+ @threat.command("list")
364
+ @click.pass_obj
365
+ @pretty_print
366
+ def list_threats(controller):
367
+ """List all scm threats"""
368
+ with Spinner(description="Fetching scm threats"):
369
+ return controller.list_threats()
370
+
371
+
372
+ @threat.command("get")
373
+ @click.argument("threat_id")
374
+ @click.pass_obj
375
+ @pretty_print
376
+ def get_threat(controller, threat_id):
377
+ """Get specific scm threat"""
378
+ with Spinner(description="Fetching scm threat"):
379
+ return controller.get_threat(id=threat_id)
380
+
381
+
382
+ @scm.command("threat-intel")
383
+ @click.argument(
384
+ "threat_pdf",
385
+ type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True),
386
+ )
387
+ @click.pass_obj
388
+ @pretty_print
389
+ def parse_threat_intel(controller, threat_pdf):
390
+ with Spinner("Parsing PDF"):
391
+ return controller.parse_threat_intel(threat_pdf)
392
+
393
+
394
+ @scm.command("from-advisory")
395
+ @click.argument(
396
+ "partner", type=click.Choice([Control.CROWDSTRIKE.name], case_sensitive=False)
397
+ )
398
+ @click.option(
399
+ "-a", "--advisory_id", required=True, type=str, help="Partner advisory ID"
400
+ )
401
+ @click.pass_obj
402
+ @pretty_print
403
+ def parse_from_partner_advisory(controller, partner, advisory_id):
404
+ with Spinner("Uploading"):
405
+ return controller.parse_from_partner_advisory(
406
+ partner=Control[partner], advisory_id=advisory_id
407
+ )
408
+
409
+
410
+ @click.group()
411
+ @click.pass_context
412
+ def exception(ctx):
413
+ """SCM exception commands"""
414
+ ctx.obj = ScmController(account=ctx.obj.account)
415
+
416
+
417
+ @click.group()
418
+ @click.pass_context
419
+ def object(ctx):
420
+ """SCM object exception commands"""
421
+ ctx.obj = ScmController(account=ctx.obj.account)
422
+
423
+
424
+ @click.group()
425
+ @click.pass_context
426
+ def policy(ctx):
427
+ """SCM policy exception commands"""
428
+ ctx.obj = ScmController(account=ctx.obj.account)
429
+
430
+
431
+ exception.add_command(object)
432
+ exception.add_command(policy)
433
+ scm.add_command(exception)
434
+
435
+
436
+ @object.command("list")
437
+ @click.pass_obj
438
+ @pretty_print
439
+ def list_object_exceptions(controller):
440
+ """List all object exceptions"""
441
+ with Spinner(description="Fetching object exceptions"):
442
+ return controller.list_object_exceptions()
443
+
444
+
445
+ @object.command("create")
446
+ @click.argument(
447
+ "category",
448
+ type=click.Choice(
449
+ [
450
+ c.name
451
+ for c in ControlCategory
452
+ if c
453
+ not in [
454
+ ControlCategory.NONE,
455
+ ControlCategory.INVALID,
456
+ ControlCategory.PRIVATE_REPO,
457
+ ]
458
+ ],
459
+ case_sensitive=False,
460
+ ),
461
+ )
462
+ @click.option(
463
+ "-f", "--filter", help="OData filter string", default=None, required=True, type=str
464
+ )
465
+ @click.option(
466
+ "-e",
467
+ "--expires",
468
+ help="expiry date (YYYY-MM-DD hh:mm:ss ([+-]hh:mm))",
469
+ default=None,
470
+ type=str,
471
+ )
472
+ @click.option("-n", "--name", help="exception name", default=None, type=str)
473
+ @click.pass_obj
474
+ @pretty_print
475
+ def create_object_exception(controller, category, filter, expires, name):
476
+ """Create object exception"""
477
+ with Spinner(description=f"Creating object exception"):
478
+ return controller.create_object_exception(
479
+ category=ControlCategory[category],
480
+ filter=filter,
481
+ name=name,
482
+ expires=expires,
483
+ )
484
+
485
+
486
+ @object.command("update")
487
+ @click.argument("exception_id", type=str)
488
+ @click.option(
489
+ "-e",
490
+ "--expires",
491
+ help="Expiry Date (YYYY-MM-DD hh:mm:ss ([+-]hh:mm))",
492
+ default=ScmController.default,
493
+ )
494
+ @click.option("-f", "--filter", help="OData filter string", default=None, type=str)
495
+ @click.option("-n", "--name", help="Exception Name", default=None, type=str)
496
+ @click.pass_obj
497
+ @pretty_print
498
+ def update_object_exception(controller, exception_id, expires, filter, name):
499
+ """Update object exception"""
500
+ with Spinner(description=f"Updating object exception"):
501
+ return controller.update_object_exception(
502
+ exception_id=exception_id, filter=filter, name=name, expires=expires
503
+ )
504
+
505
+
506
+ @object.command("delete")
507
+ @click.argument("exception_id", type=str)
508
+ @click.confirmation_option(prompt="Are you sure?")
509
+ @click.pass_obj
510
+ @pretty_print
511
+ def delete_object_exception(controller, exception_id):
512
+ """Delete object exception"""
513
+ with Spinner(description=f"Delete object exception"):
514
+ return controller.delete_object_exception(exception_id=exception_id)
515
+
516
+
517
+ @policy.command("list")
518
+ @click.pass_obj
519
+ @pretty_print
520
+ def list_policy_exceptions(controller):
521
+ """List all policy exceptions"""
522
+ with Spinner(description="Fetching policy exceptions"):
523
+ return controller.list_policy_exceptions()
524
+
525
+
526
+ @policy.command("create")
527
+ @click.argument(
528
+ "partner",
529
+ type=click.Choice(
530
+ [c.name for c in Control if c != Control.INVALID], case_sensitive=False
531
+ ),
532
+ )
533
+ @click.option("-i", "--instance_id", required=True, help="instance ID of the partner")
534
+ @click.option("-p", "--policy_id", required=True, help="ID of the policy to create")
535
+ @click.option(
536
+ "-s",
537
+ "--settings",
538
+ required=True,
539
+ help="Comma separated list of all setting names to be excluded",
540
+ )
541
+ @click.option(
542
+ "-e",
543
+ "--expires",
544
+ help="Expiry Date (YYYY-MM-DD hh:mm:ss ([+-]hh:mm))",
545
+ default=None,
546
+ type=str,
547
+ )
548
+ @click.pass_obj
549
+ @pretty_print
550
+ def create_policy_exception(
551
+ controller, partner, instance_id, policy_id, settings, expires
552
+ ):
553
+ """Create policy exception"""
554
+ with Spinner(description=f"Creating policy exception"):
555
+ return controller.create_policy_exceptions(
556
+ partner=Control[partner],
557
+ expires=expires,
558
+ instance_id=instance_id,
559
+ policy_id=policy_id,
560
+ setting_names=settings.split(",") if settings else None,
561
+ )
562
+
563
+
564
+ @policy.command("update")
565
+ @click.argument(
566
+ "partner",
567
+ type=click.Choice(
568
+ [c.name for c in Control if c != Control.INVALID], case_sensitive=False
569
+ ),
570
+ )
571
+ @click.option("-i", "--instance_id", required=True, help="instance ID of the partner")
572
+ @click.option("-p", "--policy_id", required=True, help="ID of the policy to update")
573
+ @click.option(
574
+ "-e",
575
+ "--expires",
576
+ help="Expiry Date (YYYY-MM-DD hh:mm:ss ([+-]hh:mm))",
577
+ default=ScmController.default,
578
+ )
579
+ @click.option(
580
+ "-s",
581
+ "--settings",
582
+ help="Comma separated list of all setting names to be excluded",
583
+ )
584
+ @click.pass_obj
585
+ @pretty_print
586
+ def update_policy_exception(
587
+ controller, partner, instance_id, policy_id, expires, settings
588
+ ):
589
+ """Update policy exception"""
590
+ with Spinner(description=f"Updating Policy exception"):
591
+ return controller.update_policy_exception(
592
+ partner=Control[partner],
593
+ expires=expires,
594
+ instance_id=instance_id,
595
+ policy_id=policy_id,
596
+ setting_names=settings.split(",") if settings else None,
597
+ )
598
+
599
+
600
+ @policy.command("delete")
601
+ @click.argument(
602
+ "partner",
603
+ type=click.Choice(
604
+ [c.name for c in Control if c != Control.INVALID], case_sensitive=False
605
+ ),
606
+ )
607
+ @click.option("-i", "--instance_id", required=True, help="instance ID of the partner")
608
+ @click.option("-p", "--policy_id", required=True, help="ID of the policy to be deleted")
609
+ @click.confirmation_option(prompt="Are you sure?")
610
+ @click.pass_obj
611
+ @pretty_print
612
+ def delete_policy_exception(controller, partner, instance_id, policy_id):
613
+ """Delete policy exception"""
614
+ with Spinner(description=f"Deleting Policy exception"):
615
+ return controller.delete_policy_exception(
616
+ instance_id=instance_id, policy_id=policy_id
617
+ )
618
+
619
+ """Delete policy exception removes all exceptions in a policy"""
620
+ with Spinner(description=f"Deleting policy exception"):
621
+ return controller.put_policy_exceptions(
622
+ partner=Control[partner],
623
+ expires=None,
624
+ instance_id=instance_id,
625
+ policy_id=policy_id,
626
+ setting_names=[],
627
+ )
628
+
629
+
630
+ @click.group()
631
+ @click.pass_context
632
+ def notification(ctx):
633
+ """SCM notification commands"""
634
+ ctx.obj = ScmController(account=ctx.obj.account)
635
+
636
+
637
+ scm.add_command(notification)
638
+
639
+
640
+ @notification.command("list")
641
+ @click.pass_obj
642
+ @pretty_print
643
+ def list_notifications(controller):
644
+ with Spinner("Fetching notifications"):
645
+ return controller.list_notifications()
646
+
647
+
648
+ @notification.command("delete")
649
+ @click.argument("notification_id", type=str)
650
+ @click.confirmation_option(prompt="Are you sure?")
651
+ @click.pass_obj
652
+ @pretty_print
653
+ def delete_notification(controller, notification_id):
654
+ with Spinner("Deleting notification"):
655
+ return controller.delete_notification(notification_id)
656
+
657
+
658
+ @notification.command("upsert")
659
+ @click.argument(
660
+ "control_category",
661
+ type=click.Choice([c.name for c in ControlCategory], case_sensitive=False),
662
+ )
663
+ @click.option(
664
+ "-e",
665
+ "--emails",
666
+ help="comma-separated list of emails to notify",
667
+ default=None,
668
+ type=str,
669
+ )
670
+ @click.option(
671
+ "-v",
672
+ "--event",
673
+ help="event to trigger notification for",
674
+ type=click.Choice([e.name for e in PartnerEvents], case_sensitive=False),
675
+ required=True,
676
+ )
677
+ @click.option("-f", "--filter", help="OData filter string", default=None, type=str)
678
+ @click.option(
679
+ "-i", "--id", help="ID of the notification to update", default=None, type=str
680
+ )
681
+ @click.option("-m", "--message", help="notification message", default="", type=str)
682
+ @click.option(
683
+ "-r",
684
+ "--run_code",
685
+ help="notification frequency",
686
+ type=click.Choice([r.name for r in RunCode], case_sensitive=False),
687
+ required=True,
688
+ )
689
+ @click.option(
690
+ "-s",
691
+ "--scheduled_hour",
692
+ help="scheduled UTC hour to receive notifications",
693
+ type=int,
694
+ required=True,
695
+ )
696
+ @click.option(
697
+ "--slack_urls",
698
+ help="comma-separated list of Slack Webhook URLs to notify",
699
+ default=None,
700
+ type=str,
701
+ )
702
+ @click.option(
703
+ "-p",
704
+ "--suppress_empty",
705
+ help="suppress notifications with no results",
706
+ default=True,
707
+ type=bool,
708
+ )
709
+ @click.option(
710
+ "--teams_urls",
711
+ help="comma-separated list of Teams Webhook URLs to notify",
712
+ default=None,
713
+ type=str,
714
+ )
715
+ @click.option(
716
+ "-t", "--title", help="notification title", default="SCM Notification", type=str
717
+ )
718
+ @click.pass_obj
719
+ @pretty_print
720
+ def upsert_notification(
721
+ controller,
722
+ control_category,
723
+ emails,
724
+ event,
725
+ filter,
726
+ id,
727
+ message,
728
+ run_code,
729
+ scheduled_hour,
730
+ slack_urls,
731
+ suppress_empty,
732
+ teams_urls,
733
+ title,
734
+ ):
735
+ """Upsert an SCM notification"""
736
+ with Spinner("Upserting notification"):
737
+ return controller.upsert_notification(
738
+ control_category=ControlCategory[control_category],
739
+ emails=emails.split(",") if emails else None,
740
+ event=PartnerEvents[event],
741
+ filter=filter,
742
+ id=id,
743
+ message=message,
744
+ run_code=RunCode[run_code],
745
+ scheduled_hour=scheduled_hour,
746
+ slack_urls=slack_urls.split(",") if slack_urls else None,
747
+ suppress_empty=suppress_empty,
748
+ teams_urls=teams_urls.split(",") if teams_urls else None,
749
+ title=title,
750
+ )
751
+
752
+
753
+ @scm.command("notations")
754
+ @click.pass_obj
755
+ @pretty_print
756
+ def list_notations(controller):
757
+ """List all notations"""
758
+ with Spinner("Fetching notations"):
759
+ return controller.list_notations()
760
+
761
+
762
+ @scm.command("history")
763
+ @click.option("--odata_filter", help="OData filter string", default=None)
764
+ @click.option("--start", type=str, default=None, help="start date")
765
+ @click.option("--end", type=str, default=None, help="end date")
766
+ @click.pass_obj
767
+ @pretty_print
768
+ def list_history(controller, odata_filter, start, end):
769
+ """List history"""
770
+ with Spinner("Fetching SCM history"):
771
+ return controller.list_history(start, end, filter=odata_filter)
772
+
773
+
774
+ @click.group()
775
+ @click.pass_context
776
+ def report(ctx):
777
+ """SCM report commands"""
778
+ ctx.obj = ScmController(account=ctx.obj.account)
779
+
780
+
781
+ scm.add_command(report)
782
+
783
+
784
+ @report.command("get")
785
+ @click.argument("report_id", type=str)
786
+ @click.pass_obj
787
+ @pretty_print
788
+ def get_report(controller, report_id):
789
+ with Spinner("Fetching report"):
790
+ return controller.get_report(report_id)
791
+
792
+
793
+ @report.command("list")
794
+ @click.pass_obj
795
+ @pretty_print
796
+ def list_reports(controller):
797
+ with Spinner("Fetching reports"):
798
+ return controller.list_reports()
799
+
800
+
801
+ @report.command("delete")
802
+ @click.argument("report_id", type=str)
803
+ @click.confirmation_option(prompt="Are you sure?")
804
+ @click.pass_obj
805
+ @pretty_print
806
+ def delete_report(controller, report_id):
807
+ with Spinner("Deleting report"):
808
+ return controller.delete_report(report_id)
809
+
810
+
811
+ @report.command("put")
812
+ @click.option(
813
+ "--report_data",
814
+ type=str,
815
+ help="report data in JSON format, cannot be used with report_file",
816
+ default=None,
817
+ )
818
+ @click.option(
819
+ "--report_file",
820
+ type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True),
821
+ help="report data JSON file, will ignore report_data if provided",
822
+ default=None,
823
+ )
824
+ @click.option("--report_id", type=str, help="report ID to update", default=None)
825
+ @click.pass_obj
826
+ @pretty_print
827
+ def put_report(controller, report_data, report_file, report_id):
828
+ if not report_data and not report_file:
829
+ raise ValueError("Either report_data or report_file must be provided")
830
+
831
+ with Spinner("Updating report"):
832
+ if report_file:
833
+ with open(report_file, "r") as f:
834
+ report_data = f.read()
835
+ report_data = json.loads(report_data)
836
+ return controller.put_report(report_data, report_id)
837
+
838
+
839
+ @report.command("chart-data")
840
+ @click.argument(
841
+ "scm_category",
842
+ type=click.Choice(
843
+ [c.name for c in SCMCategory if c.value > 0], case_sensitive=False
844
+ ),
845
+ )
846
+ @click.option("--group_by", "-b", help="field to group by", required=True, type=str)
847
+ @click.option(
848
+ "--group_limit", "-l", help="max number of groups to return", type=int, default=100
849
+ )
850
+ @click.option(
851
+ "--sort_by",
852
+ "-s",
853
+ help="sort method",
854
+ type=click.Choice(["a-z", "z-a", "0-9", "9-0"]),
855
+ default="9-0",
856
+ )
857
+ @click.option(
858
+ "--scopes",
859
+ "-c",
860
+ help="comma-separated list of scope to value pairs, i.e. instances/control=1,instances/platform=windows",
861
+ default=None,
862
+ type=str,
863
+ )
864
+ @click.option(
865
+ "--odata_filter", "-f", help="OData filter string", default=None, type=str
866
+ )
867
+ @click.pass_obj
868
+ @pretty_print
869
+ def get_chart_data(
870
+ controller, scm_category, group_by, group_limit, sort_by, scopes, odata_filter
871
+ ):
872
+ """Get chart data for SCM reports"""
873
+ with Spinner("Fetching chart data"):
874
+ return controller.get_chart_data(
875
+ scm_category=SCMCategory[scm_category],
876
+ group_by=group_by,
877
+ group_limit=group_limit,
878
+ sort_by=sort_by,
879
+ scopes=scopes,
880
+ odata_filter=odata_filter,
881
+ )