github-manage-security-alerts-skill 1.0.0 → 1.0.1

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.
@@ -1,835 +1,848 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import sys
5
-
6
- from github_security_common import (
7
- CODE_SCANNING_DISMISS_REASONS,
8
- DEFAULT_PAGE_SIZE,
9
- DEFAULT_SUMMARY_PAGE_SIZE,
10
- DEFAULT_SUMMARY_SAMPLE_SIZE,
11
- DEPENDABOT_DISMISS_REASONS,
12
- GitHubSecurityCliError,
13
- SECRET_SCANNING_RESOLUTIONS,
14
- )
15
-
16
- GLOBAL_FLAG_OPTIONS = {"--json"}
17
- GLOBAL_VALUE_OPTIONS = {
18
- "--api-base-url",
19
- "--repo",
20
- "--repository",
21
- "--token-env",
22
- "--web-base-url",
23
- }
24
-
25
-
26
- def parse_args() -> argparse.Namespace:
27
- """Parse CLI arguments."""
28
-
29
- parser = argparse.ArgumentParser(
30
- description=(
31
- "Inspect and manage GitHub repository security alerts using a token "
32
- "stored in an environment variable."
33
- ),
34
- formatter_class=argparse.ArgumentDefaultsHelpFormatter,
35
- )
36
- parser.add_argument(
37
- "--repo",
38
- default=".",
39
- help="Path inside the target repository checkout.",
40
- )
41
- parser.add_argument(
42
- "--repository",
43
- default=None,
44
- help="Explicit repository in owner/repo format or a GitHub repository URL.",
45
- )
46
- parser.add_argument(
47
- "--api-base-url",
48
- default=None,
49
- help="Explicit GitHub API base URL override.",
50
- )
51
- parser.add_argument(
52
- "--web-base-url",
53
- default=None,
54
- help="Explicit GitHub web base URL override.",
55
- )
56
- parser.add_argument(
57
- "--token-env",
58
- action="append",
59
- dest="token_envs",
60
- default=None,
61
- help=(
62
- "Environment variable name that may contain the GitHub token. "
63
- "Repeat to provide fallbacks."
64
- ),
65
- )
66
- parser.add_argument(
67
- "--json",
68
- action="store_true",
69
- help="Emit JSON instead of human-readable text.",
70
- )
71
-
72
- subparsers = parser.add_subparsers(dest="command", required=True)
73
-
74
- summary_parser = subparsers.add_parser(
75
- "summary",
76
- help="Fetch a cross-surface summary of repository security alerts.",
77
- )
78
- summary_parser.add_argument(
79
- "--sample-size",
80
- type=int,
81
- default=DEFAULT_SUMMARY_SAMPLE_SIZE,
82
- help="Number of sample alerts to include per alert family.",
83
- )
84
- summary_parser.add_argument(
85
- "--per-page",
86
- type=int,
87
- default=DEFAULT_SUMMARY_PAGE_SIZE,
88
- help="Maximum alerts to inspect per alert family for the summary.",
89
- )
90
-
91
- subparsers.add_parser(
92
- "repo-security-overview",
93
- help="Inspect repository security_and_analysis settings and basic repository metadata.",
94
- )
95
- add_export_alerts_parser(subparsers)
96
- add_bulk_update_alerts_parser(subparsers)
97
- add_code_scanning_list_parser(subparsers)
98
- add_code_scanning_show_parser(subparsers)
99
- add_code_scanning_update_parser(subparsers)
100
- add_dependabot_list_parser(subparsers)
101
- add_dependabot_show_parser(subparsers)
102
- add_dependabot_update_parser(subparsers)
103
- add_malware_list_parser(subparsers)
104
- add_malware_show_parser(subparsers)
105
- add_malware_update_parser(subparsers)
106
- add_secret_scanning_list_parser(subparsers)
107
- add_secret_scanning_show_parser(subparsers)
108
- add_secret_scanning_update_parser(subparsers)
109
- add_secret_locations_parser(subparsers)
110
- subparsers.add_parser(
111
- "secret-scan-history",
112
- help="Inspect the latest secret scanning scan history for the repository.",
113
- )
114
- add_api_call_parser(subparsers)
115
-
116
- return parser.parse_args(normalize_global_argument_order(sys.argv[1:]))
117
-
118
-
119
- def normalize_global_argument_order(arguments: list[str]) -> list[str]:
120
- """Allow global options to appear before or after the subcommand."""
121
-
122
- normalized_prefix: list[str] = []
123
- normalized_suffix: list[str] = []
124
- index = 0
125
-
126
- while index < len(arguments):
127
- argument = arguments[index]
128
-
129
- if argument in GLOBAL_FLAG_OPTIONS:
130
- normalized_prefix.append(argument)
131
- index += 1
132
- continue
133
-
134
- if argument in GLOBAL_VALUE_OPTIONS:
135
- if index + 1 >= len(arguments):
136
- raise GitHubSecurityCliError(
137
- f"Expected a value after global option '{argument}'."
138
- )
139
- normalized_prefix.extend([argument, arguments[index + 1]])
140
- index += 2
141
- continue
142
-
143
- normalized_suffix.append(argument)
144
- index += 1
145
-
146
- return [*normalized_prefix, *normalized_suffix]
147
-
148
-
149
- def add_export_alerts_parser(
150
- subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
151
- ) -> None:
152
- parser = subparsers.add_parser(
153
- "export-alerts",
154
- help="Export full alert collections across supported GitHub security surfaces.",
155
- )
156
- parser.add_argument(
157
- "--per-page",
158
- type=int,
159
- default=DEFAULT_SUMMARY_PAGE_SIZE,
160
- help="Maximum number of alerts to request from each alert family.",
161
- )
162
- parser.add_argument(
163
- "--code-scanning-state",
164
- default=None,
165
- help="Optional code scanning state filter.",
166
- )
167
- parser.add_argument(
168
- "--dependabot-state",
169
- default=None,
170
- help="Optional Dependabot state filter.",
171
- )
172
- parser.add_argument(
173
- "--secret-scanning-state",
174
- default=None,
175
- help="Optional secret scanning state filter.",
176
- )
177
- parser.add_argument(
178
- "--show-secret-values",
179
- action="store_true",
180
- help="Do not request secret redaction for secret scanning exports.",
181
- )
182
-
183
-
184
- def add_bulk_update_alerts_parser(
185
- subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
186
- ) -> None:
187
- parser = subparsers.add_parser(
188
- "bulk-update-alerts",
189
- help="Bulk dismiss, reopen, resolve, or assign alerts across one security surface.",
190
- )
191
- parser.add_argument(
192
- "--surface",
193
- required=True,
194
- choices=(
195
- "code-scanning",
196
- "dependabot",
197
- "malware",
198
- "secret-scanning",
199
- ),
200
- help="Alert surface to target.",
201
- )
202
- parser.add_argument(
203
- "--alert",
204
- action="append",
205
- dest="alerts",
206
- type=int,
207
- default=None,
208
- help="Explicit alert number to update. Repeat for multiple alerts.",
209
- )
210
- parser.add_argument(
211
- "--limit",
212
- type=int,
213
- default=None,
214
- help="Maximum number of matched alerts to update.",
215
- )
216
- parser.add_argument(
217
- "--select-state",
218
- default=None,
219
- help="Current-state filter used when selecting alerts by query.",
220
- )
221
- parser.add_argument(
222
- "--severity",
223
- default=None,
224
- help="Severity filter for code scanning or Dependabot selection.",
225
- )
226
- parser.add_argument(
227
- "--assignee-filter",
228
- default=None,
229
- help="Assignee filter used during alert selection.",
230
- )
231
- parser.add_argument(
232
- "--target-state",
233
- default=None,
234
- help="Desired new state. When omitted, assignment-only changes reuse each alert's current state.",
235
- )
236
- parser.add_argument(
237
- "--dismissed-reason",
238
- default=None,
239
- help="Dismissal reason for code scanning, Dependabot, or malware alerts.",
240
- )
241
- parser.add_argument(
242
- "--resolution",
243
- default=None,
244
- help="Resolution when bulk-resolving secret scanning alerts.",
245
- )
246
- parser.add_argument(
247
- "--comment",
248
- default=None,
249
- help="Optional dismissal or resolution comment.",
250
- )
251
- parser.add_argument(
252
- "--assignee",
253
- action="append",
254
- dest="assignees",
255
- default=None,
256
- help="Assignee login to apply. Repeat where the surface supports multiple assignees.",
257
- )
258
- parser.add_argument(
259
- "--clear-assignees",
260
- action="store_true",
261
- help="Remove all assignees, or unassign for secret scanning.",
262
- )
263
- parser.add_argument(
264
- "--create-request",
265
- action="store_true",
266
- help="Request a dismissal request when bulk-updating code scanning alerts.",
267
- )
268
- parser.add_argument(
269
- "--skip-malware-check",
270
- action="store_true",
271
- help="Skip malware advisory verification when the surface is malware.",
272
- )
273
- parser.add_argument(
274
- "--show-secret-values",
275
- action="store_true",
276
- help="Do not request secret redaction when selecting secret scanning alerts.",
277
- )
278
- parser.add_argument(
279
- "--tool-name",
280
- default=None,
281
- help="Code scanning tool-name filter.",
282
- )
283
- parser.add_argument(
284
- "--tool-guid",
285
- default=None,
286
- help="Code scanning tool-GUID filter.",
287
- )
288
- parser.add_argument(
289
- "--ref",
290
- default=None,
291
- help="Code scanning ref filter.",
292
- )
293
- parser.add_argument(
294
- "--pr",
295
- type=int,
296
- default=None,
297
- help="Code scanning pull-request filter.",
298
- )
299
- parser.add_argument(
300
- "--ecosystem",
301
- default=None,
302
- help="Dependabot or malware ecosystem filter.",
303
- )
304
- parser.add_argument(
305
- "--package",
306
- default=None,
307
- help="Dependabot or malware package-name filter.",
308
- )
309
- parser.add_argument(
310
- "--manifest",
311
- default=None,
312
- help="Dependabot or malware manifest-path filter.",
313
- )
314
- parser.add_argument(
315
- "--epss-percentage",
316
- default=None,
317
- help="Dependabot or malware EPSS filter.",
318
- )
319
- parser.add_argument(
320
- "--has",
321
- dest="has_filter",
322
- default=None,
323
- help="Dependabot or malware has-filter, for example patch.",
324
- )
325
- parser.add_argument(
326
- "--scope",
327
- default=None,
328
- help="Dependabot or malware dependency-scope filter.",
329
- )
330
- parser.add_argument(
331
- "--before",
332
- default=None,
333
- help="Dependabot or malware cursor filter.",
334
- )
335
- parser.add_argument(
336
- "--after",
337
- default=None,
338
- help="Dependabot or malware cursor filter.",
339
- )
340
- parser.add_argument(
341
- "--secret-type",
342
- default=None,
343
- help="Secret scanning secret-type filter.",
344
- )
345
- parser.add_argument(
346
- "--resolution-filter",
347
- default=None,
348
- help="Secret scanning current-resolution filter.",
349
- )
350
- parser.add_argument(
351
- "--validity",
352
- default=None,
353
- help="Secret scanning validity filter.",
354
- )
355
- parser.add_argument(
356
- "--is-publicly-leaked",
357
- action="store_true",
358
- help="Filter secret scanning alerts to publicly leaked ones.",
359
- )
360
- parser.add_argument(
361
- "--is-multi-repo",
362
- action="store_true",
363
- help="Filter secret scanning alerts to multi-repo ones.",
364
- )
365
- parser.add_argument(
366
- "--sort",
367
- default=None,
368
- help="Surface-specific sort field.",
369
- )
370
- parser.add_argument(
371
- "--direction",
372
- default=None,
373
- help="Surface-specific sort direction.",
374
- )
375
- parser.add_argument(
376
- "--page",
377
- type=int,
378
- default=1,
379
- help="Surface-specific page number for code or secret scanning selection.",
380
- )
381
- parser.add_argument(
382
- "--per-page",
383
- type=int,
384
- default=DEFAULT_SUMMARY_PAGE_SIZE,
385
- help="Maximum number of alerts to request during selection.",
386
- )
387
- parser.add_argument(
388
- "--dry-run",
389
- action="store_true",
390
- help="Show the selected alerts and planned payload without mutating anything.",
391
- )
392
-
393
-
394
- def add_code_scanning_common_filters(parser: argparse.ArgumentParser) -> None:
395
- parser.add_argument(
396
- "--tool-name", default=None, help="Filter by tool name."
397
- )
398
- parser.add_argument(
399
- "--tool-guid", default=None, help="Filter by tool GUID."
400
- )
401
- parser.add_argument("--state", default=None, help="Alert state filter.")
402
- parser.add_argument("--severity", default=None, help="Severity filter.")
403
- parser.add_argument("--assignees", default=None, help="Assignee filter.")
404
- parser.add_argument("--ref", default=None, help="Git ref filter.")
405
- parser.add_argument(
406
- "--pr", type=int, default=None, help="Pull request number filter."
407
- )
408
- parser.add_argument("--sort", default=None, help="Sort field.")
409
- parser.add_argument("--direction", default=None, help="Sort direction.")
410
- parser.add_argument("--page", type=int, default=1, help="Page number.")
411
- parser.add_argument(
412
- "--per-page",
413
- type=int,
414
- default=DEFAULT_PAGE_SIZE,
415
- help="Number of alerts per page.",
416
- )
417
-
418
-
419
- def add_code_scanning_list_parser(
420
- subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
421
- ) -> None:
422
- parser = subparsers.add_parser(
423
- "list-code-scanning",
424
- help="List code scanning alerts for a repository.",
425
- )
426
- add_code_scanning_common_filters(parser)
427
-
428
-
429
- def add_code_scanning_show_parser(
430
- subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
431
- ) -> None:
432
- parser = subparsers.add_parser(
433
- "show-code-scanning",
434
- help="Show one code scanning alert, optionally with instances and autofix status.",
435
- )
436
- parser.add_argument(
437
- "--alert", required=True, type=int, help="Code scanning alert number."
438
- )
439
- parser.add_argument(
440
- "--include-instances",
441
- action="store_true",
442
- help="Also fetch alert instances.",
443
- )
444
- parser.add_argument(
445
- "--include-autofix",
446
- action="store_true",
447
- help="Also fetch autofix status.",
448
- )
449
- parser.add_argument(
450
- "--instances-per-page",
451
- type=int,
452
- default=DEFAULT_PAGE_SIZE,
453
- help="Instances page size when --include-instances is used.",
454
- )
455
-
456
-
457
- def add_code_scanning_update_parser(
458
- subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
459
- ) -> None:
460
- parser = subparsers.add_parser(
461
- "update-code-scanning",
462
- help="Dismiss, reopen, or reassign a code scanning alert.",
463
- )
464
- parser.add_argument(
465
- "--alert", required=True, type=int, help="Code scanning alert number."
466
- )
467
- parser.add_argument(
468
- "--state",
469
- required=True,
470
- choices=("open", "dismissed"),
471
- help="Desired alert state.",
472
- )
473
- parser.add_argument(
474
- "--dismissed-reason",
475
- default=None,
476
- choices=CODE_SCANNING_DISMISS_REASONS,
477
- help="Dismissal reason when state is dismissed.",
478
- )
479
- parser.add_argument(
480
- "--comment", default=None, help="Optional dismissal comment."
481
- )
482
- parser.add_argument(
483
- "--assignee",
484
- action="append",
485
- dest="assignees",
486
- default=None,
487
- help="Assignee login to apply. Repeat for multiple assignees.",
488
- )
489
- parser.add_argument(
490
- "--clear-assignees",
491
- action="store_true",
492
- help="Remove all assignees.",
493
- )
494
- parser.add_argument(
495
- "--create-request",
496
- action="store_true",
497
- help="Ask GitHub to create an alert dismissal request when supported.",
498
- )
499
- parser.add_argument(
500
- "--dry-run",
501
- action="store_true",
502
- help="Print the intended mutation without sending it.",
503
- )
504
-
505
-
506
- def add_dependabot_common_filters(parser: argparse.ArgumentParser) -> None:
507
- parser.add_argument("--state", default=None, help="Alert state filter.")
508
- parser.add_argument("--severity", default=None, help="Severity filter.")
509
- parser.add_argument("--ecosystem", default=None, help="Ecosystem filter.")
510
- parser.add_argument("--package", default=None, help="Package-name filter.")
511
- parser.add_argument(
512
- "--manifest", default=None, help="Manifest-path filter."
513
- )
514
- parser.add_argument("--epss-percentage", default=None, help="EPSS filter.")
515
- parser.add_argument(
516
- "--has",
517
- dest="has_filter",
518
- default=None,
519
- help="Has filter, for example patch.",
520
- )
521
- parser.add_argument("--assignee", default=None, help="Assignee filter.")
522
- parser.add_argument(
523
- "--scope", default=None, help="Dependency scope filter."
524
- )
525
- parser.add_argument("--sort", default=None, help="Sort field.")
526
- parser.add_argument("--direction", default=None, help="Sort direction.")
527
- parser.add_argument(
528
- "--before", default=None, help="Cursor for the previous page."
529
- )
530
- parser.add_argument(
531
- "--after", default=None, help="Cursor for the next page."
532
- )
533
- parser.add_argument(
534
- "--per-page",
535
- type=int,
536
- default=DEFAULT_PAGE_SIZE,
537
- help="Number of alerts per page.",
538
- )
539
-
540
-
541
- def add_dependabot_list_parser(
542
- subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
543
- ) -> None:
544
- parser = subparsers.add_parser(
545
- "list-dependabot",
546
- help="List Dependabot alerts for a repository.",
547
- )
548
- add_dependabot_common_filters(parser)
549
-
550
-
551
- def add_dependabot_show_parser(
552
- subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
553
- ) -> None:
554
- parser = subparsers.add_parser(
555
- "show-dependabot", help="Show one Dependabot alert."
556
- )
557
- parser.add_argument(
558
- "--alert", required=True, type=int, help="Dependabot alert number."
559
- )
560
-
561
-
562
- def add_dependabot_update_parser(
563
- subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
564
- ) -> None:
565
- parser = subparsers.add_parser(
566
- "update-dependabot",
567
- help="Dismiss, reopen, or reassign a Dependabot alert.",
568
- )
569
- parser.add_argument(
570
- "--alert", required=True, type=int, help="Dependabot alert number."
571
- )
572
- parser.add_argument(
573
- "--state",
574
- required=True,
575
- choices=("open", "dismissed"),
576
- help="Desired alert state.",
577
- )
578
- parser.add_argument(
579
- "--dismissed-reason",
580
- default=None,
581
- choices=DEPENDABOT_DISMISS_REASONS,
582
- help="Dismissal reason when state is dismissed.",
583
- )
584
- parser.add_argument(
585
- "--comment", default=None, help="Optional dismissal comment."
586
- )
587
- parser.add_argument(
588
- "--assignee",
589
- action="append",
590
- dest="assignees",
591
- default=None,
592
- help="Assignee login to apply. Repeat for multiple assignees.",
593
- )
594
- parser.add_argument(
595
- "--clear-assignees",
596
- action="store_true",
597
- help="Remove all assignees.",
598
- )
599
- parser.add_argument(
600
- "--dry-run",
601
- action="store_true",
602
- help="Print the intended mutation without sending it.",
603
- )
604
-
605
-
606
- def add_malware_list_parser(
607
- subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
608
- ) -> None:
609
- parser = subparsers.add_parser(
610
- "list-malware",
611
- help="List Dependabot malware alerts for a repository.",
612
- )
613
- add_dependabot_common_filters(parser)
614
-
615
-
616
- def add_malware_show_parser(
617
- subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
618
- ) -> None:
619
- parser = subparsers.add_parser(
620
- "show-malware",
621
- help="Show one malware alert, backed by a Dependabot alert whose advisory type is malware.",
622
- )
623
- parser.add_argument(
624
- "--alert", required=True, type=int, help="Alert number."
625
- )
626
-
627
-
628
- def add_malware_update_parser(
629
- subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
630
- ) -> None:
631
- parser = subparsers.add_parser(
632
- "update-malware",
633
- help="Dismiss, reopen, or reassign a malware alert via the Dependabot alert API.",
634
- )
635
- parser.add_argument(
636
- "--alert", required=True, type=int, help="Alert number."
637
- )
638
- parser.add_argument(
639
- "--state",
640
- required=True,
641
- choices=("open", "dismissed"),
642
- help="Desired alert state.",
643
- )
644
- parser.add_argument(
645
- "--dismissed-reason",
646
- default=None,
647
- choices=DEPENDABOT_DISMISS_REASONS,
648
- help="Dismissal reason when state is dismissed.",
649
- )
650
- parser.add_argument(
651
- "--comment", default=None, help="Optional dismissal comment."
652
- )
653
- parser.add_argument(
654
- "--assignee",
655
- action="append",
656
- dest="assignees",
657
- default=None,
658
- help="Assignee login to apply. Repeat for multiple assignees.",
659
- )
660
- parser.add_argument(
661
- "--clear-assignees",
662
- action="store_true",
663
- help="Remove all assignees.",
664
- )
665
- parser.add_argument(
666
- "--dry-run",
667
- action="store_true",
668
- help="Print the intended mutation without sending it.",
669
- )
670
- parser.add_argument(
671
- "--skip-malware-check",
672
- action="store_true",
673
- help="Apply the update without verifying the alert maps to a malware advisory.",
674
- )
675
-
676
-
677
- def add_secret_scanning_common_filters(
678
- parser: argparse.ArgumentParser,
679
- ) -> None:
680
- parser.add_argument("--state", default=None, help="Alert state filter.")
681
- parser.add_argument(
682
- "--secret-type", default=None, help="Secret type filter."
683
- )
684
- parser.add_argument(
685
- "--resolution", default=None, help="Resolution filter."
686
- )
687
- parser.add_argument("--assignee", default=None, help="Assignee filter.")
688
- parser.add_argument("--validity", default=None, help="Validity filter.")
689
- parser.add_argument(
690
- "--is-publicly-leaked",
691
- action="store_true",
692
- help="Filter to publicly leaked alerts.",
693
- )
694
- parser.add_argument(
695
- "--is-multi-repo",
696
- action="store_true",
697
- help="Filter to multi-repo alerts.",
698
- )
699
- parser.add_argument("--sort", default=None, help="Sort field.")
700
- parser.add_argument("--direction", default=None, help="Sort direction.")
701
- parser.add_argument("--page", type=int, default=1, help="Page number.")
702
- parser.add_argument(
703
- "--per-page",
704
- type=int,
705
- default=DEFAULT_PAGE_SIZE,
706
- help="Number of alerts per page.",
707
- )
708
- parser.add_argument(
709
- "--show-secret-values",
710
- action="store_true",
711
- help="Do not request secret redaction from the API.",
712
- )
713
-
714
-
715
- def add_secret_scanning_list_parser(
716
- subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
717
- ) -> None:
718
- parser = subparsers.add_parser(
719
- "list-secret-scanning",
720
- help="List secret scanning alerts for a repository.",
721
- )
722
- add_secret_scanning_common_filters(parser)
723
-
724
-
725
- def add_secret_scanning_show_parser(
726
- subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
727
- ) -> None:
728
- parser = subparsers.add_parser(
729
- "show-secret-scanning", help="Show one secret scanning alert."
730
- )
731
- parser.add_argument(
732
- "--alert",
733
- required=True,
734
- type=int,
735
- help="Secret scanning alert number.",
736
- )
737
- parser.add_argument(
738
- "--show-secret-values",
739
- action="store_true",
740
- help="Do not request secret redaction from the API.",
741
- )
742
-
743
-
744
- def add_secret_scanning_update_parser(
745
- subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
746
- ) -> None:
747
- parser = subparsers.add_parser(
748
- "update-secret-scanning",
749
- help="Resolve, reopen, or reassign a secret scanning alert.",
750
- )
751
- parser.add_argument(
752
- "--alert",
753
- required=True,
754
- type=int,
755
- help="Secret scanning alert number.",
756
- )
757
- parser.add_argument(
758
- "--state",
759
- required=True,
760
- choices=("open", "resolved"),
761
- help="Desired alert state.",
762
- )
763
- parser.add_argument(
764
- "--resolution",
765
- default=None,
766
- choices=SECRET_SCANNING_RESOLUTIONS,
767
- help="Resolution when state is resolved.",
768
- )
769
- parser.add_argument(
770
- "--comment", default=None, help="Optional resolution comment."
771
- )
772
- parser.add_argument(
773
- "--assignee",
774
- default=None,
775
- help="Assign the alert to this user login.",
776
- )
777
- parser.add_argument(
778
- "--unassign",
779
- action="store_true",
780
- help="Remove the current assignee.",
781
- )
782
- parser.add_argument(
783
- "--dry-run",
784
- action="store_true",
785
- help="Print the intended mutation without sending it.",
786
- )
787
-
788
-
789
- def add_secret_locations_parser(
790
- subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
791
- ) -> None:
792
- parser = subparsers.add_parser(
793
- "list-secret-locations",
794
- help="List all known locations for a secret scanning alert.",
795
- )
796
- parser.add_argument(
797
- "--alert",
798
- required=True,
799
- type=int,
800
- help="Secret scanning alert number.",
801
- )
802
- parser.add_argument("--page", type=int, default=1, help="Page number.")
803
- parser.add_argument(
804
- "--per-page",
805
- type=int,
806
- default=DEFAULT_PAGE_SIZE,
807
- help="Number of locations per page.",
808
- )
809
-
810
-
811
- def add_api_call_parser(
812
- subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
813
- ) -> None:
814
- parser = subparsers.add_parser(
815
- "api-call",
816
- help="Raw GitHub API fallback for anything not wrapped yet.",
817
- )
818
- parser.add_argument(
819
- "--endpoint",
820
- required=True,
821
- help="API endpoint path, for example /repos/OWNER/REPO/code-scanning/default-setup.",
822
- )
823
- parser.add_argument("--method", default="GET", help="HTTP method.")
824
- parser.add_argument(
825
- "--query-param",
826
- action="append",
827
- dest="query_params",
828
- default=None,
829
- help="Query parameter in key=value form. Repeat for multiple parameters.",
830
- )
831
- parser.add_argument(
832
- "--body-json",
833
- default=None,
834
- help="Optional JSON request body for POST, PATCH, or PUT operations.",
835
- )
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from github_security_common import (
7
+ CODE_SCANNING_DISMISS_REASONS,
8
+ DEFAULT_PAGE_SIZE,
9
+ DEFAULT_SUMMARY_PAGE_SIZE,
10
+ DEFAULT_SUMMARY_SAMPLE_SIZE,
11
+ DEPENDABOT_DISMISS_REASONS,
12
+ GitHubSecurityCliError,
13
+ SECRET_SCANNING_RESOLUTIONS,
14
+ )
15
+
16
+ GLOBAL_FLAG_OPTIONS = {"--json"}
17
+ GLOBAL_VALUE_OPTIONS = {
18
+ "--api-base-url",
19
+ "--repo",
20
+ "--repository",
21
+ "--token-env",
22
+ "--web-base-url",
23
+ }
24
+ HELP_ALERT_STATE_FILTER = "Alert state filter."
25
+ HELP_ALERTS_PER_PAGE = "Number of alerts per page."
26
+ HELP_ASSIGNEE_FILTER = "Assignee filter."
27
+ HELP_ASSIGNEE_REPEAT = "Assignee login to apply. Repeat for multiple assignees."
28
+ HELP_DESIRED_ALERT_STATE = "Desired alert state."
29
+ HELP_DISMISSAL_COMMENT = "Optional dismissal comment."
30
+ HELP_DISMISSAL_REASON = "Dismissal reason when state is dismissed."
31
+ HELP_DRY_RUN = "Print the intended mutation without sending it."
32
+ HELP_PAGE_NUMBER = "Page number."
33
+ HELP_REMOVE_ASSIGNEES = "Remove all assignees."
34
+ HELP_SECRET_ALERT_NUMBER = "Secret scanning alert number."
35
+ HELP_SORT_DIRECTION = "Sort direction."
36
+ HELP_SORT_FIELD = "Sort field."
37
+
38
+
39
+ def parse_args() -> argparse.Namespace:
40
+ """Parse CLI arguments."""
41
+
42
+ parser = argparse.ArgumentParser(
43
+ description=(
44
+ "Inspect and manage GitHub repository security alerts using a token "
45
+ "stored in an environment variable."
46
+ ),
47
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
48
+ )
49
+ parser.add_argument(
50
+ "--repo",
51
+ default=".",
52
+ help="Path inside the target repository checkout.",
53
+ )
54
+ parser.add_argument(
55
+ "--repository",
56
+ default=None,
57
+ help="Explicit repository in owner/repo format or a GitHub repository URL.",
58
+ )
59
+ parser.add_argument(
60
+ "--api-base-url",
61
+ default=None,
62
+ help="Explicit GitHub API base URL override.",
63
+ )
64
+ parser.add_argument(
65
+ "--web-base-url",
66
+ default=None,
67
+ help="Explicit GitHub web base URL override.",
68
+ )
69
+ parser.add_argument(
70
+ "--token-env",
71
+ action="append",
72
+ dest="token_envs",
73
+ default=None,
74
+ help=(
75
+ "Environment variable name that may contain the GitHub token. "
76
+ "Repeat to provide fallbacks."
77
+ ),
78
+ )
79
+ parser.add_argument(
80
+ "--json",
81
+ action="store_true",
82
+ help="Emit JSON instead of human-readable text.",
83
+ )
84
+
85
+ subparsers = parser.add_subparsers(dest="command", required=True)
86
+
87
+ summary_parser = subparsers.add_parser(
88
+ "summary",
89
+ help="Fetch a cross-surface summary of repository security alerts.",
90
+ )
91
+ summary_parser.add_argument(
92
+ "--sample-size",
93
+ type=int,
94
+ default=DEFAULT_SUMMARY_SAMPLE_SIZE,
95
+ help="Number of sample alerts to include per alert family.",
96
+ )
97
+ summary_parser.add_argument(
98
+ "--per-page",
99
+ type=int,
100
+ default=DEFAULT_SUMMARY_PAGE_SIZE,
101
+ help="Maximum alerts to inspect per alert family for the summary.",
102
+ )
103
+
104
+ subparsers.add_parser(
105
+ "repo-security-overview",
106
+ help="Inspect repository security_and_analysis settings and basic repository metadata.",
107
+ )
108
+ add_export_alerts_parser(subparsers)
109
+ add_bulk_update_alerts_parser(subparsers)
110
+ add_code_scanning_list_parser(subparsers)
111
+ add_code_scanning_show_parser(subparsers)
112
+ add_code_scanning_update_parser(subparsers)
113
+ add_dependabot_list_parser(subparsers)
114
+ add_dependabot_show_parser(subparsers)
115
+ add_dependabot_update_parser(subparsers)
116
+ add_malware_list_parser(subparsers)
117
+ add_malware_show_parser(subparsers)
118
+ add_malware_update_parser(subparsers)
119
+ add_secret_scanning_list_parser(subparsers)
120
+ add_secret_scanning_show_parser(subparsers)
121
+ add_secret_scanning_update_parser(subparsers)
122
+ add_secret_locations_parser(subparsers)
123
+ subparsers.add_parser(
124
+ "secret-scan-history",
125
+ help="Inspect the latest secret scanning scan history for the repository.",
126
+ )
127
+ add_api_call_parser(subparsers)
128
+
129
+ return parser.parse_args(normalize_global_argument_order(sys.argv[1:]))
130
+
131
+
132
+ def normalize_global_argument_order(arguments: list[str]) -> list[str]:
133
+ """Allow global options to appear before or after the subcommand."""
134
+
135
+ normalized_prefix: list[str] = []
136
+ normalized_suffix: list[str] = []
137
+ index = 0
138
+
139
+ while index < len(arguments):
140
+ argument = arguments[index]
141
+
142
+ if argument in GLOBAL_FLAG_OPTIONS:
143
+ normalized_prefix.append(argument)
144
+ index += 1
145
+ continue
146
+
147
+ if argument in GLOBAL_VALUE_OPTIONS:
148
+ if index + 1 >= len(arguments):
149
+ raise GitHubSecurityCliError(
150
+ f"Expected a value after global option '{argument}'."
151
+ )
152
+ normalized_prefix.extend([argument, arguments[index + 1]])
153
+ index += 2
154
+ continue
155
+
156
+ normalized_suffix.append(argument)
157
+ index += 1
158
+
159
+ return [*normalized_prefix, *normalized_suffix]
160
+
161
+
162
+ def add_export_alerts_parser(
163
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
164
+ ) -> None:
165
+ parser = subparsers.add_parser(
166
+ "export-alerts",
167
+ help="Export full alert collections across supported GitHub security surfaces.",
168
+ )
169
+ parser.add_argument(
170
+ "--per-page",
171
+ type=int,
172
+ default=DEFAULT_SUMMARY_PAGE_SIZE,
173
+ help="Maximum number of alerts to request from each alert family.",
174
+ )
175
+ parser.add_argument(
176
+ "--code-scanning-state",
177
+ default=None,
178
+ help="Optional code scanning state filter.",
179
+ )
180
+ parser.add_argument(
181
+ "--dependabot-state",
182
+ default=None,
183
+ help="Optional Dependabot state filter.",
184
+ )
185
+ parser.add_argument(
186
+ "--secret-scanning-state",
187
+ default=None,
188
+ help="Optional secret scanning state filter.",
189
+ )
190
+ parser.add_argument(
191
+ "--show-secret-values",
192
+ action="store_true",
193
+ help="Do not request secret redaction for secret scanning exports.",
194
+ )
195
+
196
+
197
+ def add_bulk_update_alerts_parser(
198
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
199
+ ) -> None:
200
+ parser = subparsers.add_parser(
201
+ "bulk-update-alerts",
202
+ help="Bulk dismiss, reopen, resolve, or assign alerts across one security surface.",
203
+ )
204
+ parser.add_argument(
205
+ "--surface",
206
+ required=True,
207
+ choices=(
208
+ "code-scanning",
209
+ "dependabot",
210
+ "malware",
211
+ "secret-scanning",
212
+ ),
213
+ help="Alert surface to target.",
214
+ )
215
+ parser.add_argument(
216
+ "--alert",
217
+ action="append",
218
+ dest="alerts",
219
+ type=int,
220
+ default=None,
221
+ help="Explicit alert number to update. Repeat for multiple alerts.",
222
+ )
223
+ parser.add_argument(
224
+ "--limit",
225
+ type=int,
226
+ default=None,
227
+ help="Maximum number of matched alerts to update.",
228
+ )
229
+ parser.add_argument(
230
+ "--select-state",
231
+ default=None,
232
+ help="Current-state filter used when selecting alerts by query.",
233
+ )
234
+ parser.add_argument(
235
+ "--severity",
236
+ default=None,
237
+ help="Severity filter for code scanning or Dependabot selection.",
238
+ )
239
+ parser.add_argument(
240
+ "--assignee-filter",
241
+ default=None,
242
+ help="Assignee filter used during alert selection.",
243
+ )
244
+ parser.add_argument(
245
+ "--target-state",
246
+ default=None,
247
+ help="Desired new state. When omitted, assignment-only changes reuse each alert's current state.",
248
+ )
249
+ parser.add_argument(
250
+ "--dismissed-reason",
251
+ default=None,
252
+ help="Dismissal reason for code scanning, Dependabot, or malware alerts.",
253
+ )
254
+ parser.add_argument(
255
+ "--resolution",
256
+ default=None,
257
+ help="Resolution when bulk-resolving secret scanning alerts.",
258
+ )
259
+ parser.add_argument(
260
+ "--comment",
261
+ default=None,
262
+ help="Optional dismissal or resolution comment.",
263
+ )
264
+ parser.add_argument(
265
+ "--assignee",
266
+ action="append",
267
+ dest="assignees",
268
+ default=None,
269
+ help="Assignee login to apply. Repeat where the surface supports multiple assignees.",
270
+ )
271
+ parser.add_argument(
272
+ "--clear-assignees",
273
+ action="store_true",
274
+ help="Remove all assignees, or unassign for secret scanning.",
275
+ )
276
+ parser.add_argument(
277
+ "--create-request",
278
+ action="store_true",
279
+ help="Request a dismissal request when bulk-updating code scanning alerts.",
280
+ )
281
+ parser.add_argument(
282
+ "--skip-malware-check",
283
+ action="store_true",
284
+ help="Skip malware advisory verification when the surface is malware.",
285
+ )
286
+ parser.add_argument(
287
+ "--show-secret-values",
288
+ action="store_true",
289
+ help="Do not request secret redaction when selecting secret scanning alerts.",
290
+ )
291
+ parser.add_argument(
292
+ "--tool-name",
293
+ default=None,
294
+ help="Code scanning tool-name filter.",
295
+ )
296
+ parser.add_argument(
297
+ "--tool-guid",
298
+ default=None,
299
+ help="Code scanning tool-GUID filter.",
300
+ )
301
+ parser.add_argument(
302
+ "--ref",
303
+ default=None,
304
+ help="Code scanning ref filter.",
305
+ )
306
+ parser.add_argument(
307
+ "--pr",
308
+ type=int,
309
+ default=None,
310
+ help="Code scanning pull-request filter.",
311
+ )
312
+ parser.add_argument(
313
+ "--ecosystem",
314
+ default=None,
315
+ help="Dependabot or malware ecosystem filter.",
316
+ )
317
+ parser.add_argument(
318
+ "--package",
319
+ default=None,
320
+ help="Dependabot or malware package-name filter.",
321
+ )
322
+ parser.add_argument(
323
+ "--manifest",
324
+ default=None,
325
+ help="Dependabot or malware manifest-path filter.",
326
+ )
327
+ parser.add_argument(
328
+ "--epss-percentage",
329
+ default=None,
330
+ help="Dependabot or malware EPSS filter.",
331
+ )
332
+ parser.add_argument(
333
+ "--has",
334
+ dest="has_filter",
335
+ default=None,
336
+ help="Dependabot or malware has-filter, for example patch.",
337
+ )
338
+ parser.add_argument(
339
+ "--scope",
340
+ default=None,
341
+ help="Dependabot or malware dependency-scope filter.",
342
+ )
343
+ parser.add_argument(
344
+ "--before",
345
+ default=None,
346
+ help="Dependabot or malware cursor filter.",
347
+ )
348
+ parser.add_argument(
349
+ "--after",
350
+ default=None,
351
+ help="Dependabot or malware cursor filter.",
352
+ )
353
+ parser.add_argument(
354
+ "--secret-type",
355
+ default=None,
356
+ help="Secret scanning secret-type filter.",
357
+ )
358
+ parser.add_argument(
359
+ "--resolution-filter",
360
+ default=None,
361
+ help="Secret scanning current-resolution filter.",
362
+ )
363
+ parser.add_argument(
364
+ "--validity",
365
+ default=None,
366
+ help="Secret scanning validity filter.",
367
+ )
368
+ parser.add_argument(
369
+ "--is-publicly-leaked",
370
+ action="store_true",
371
+ help="Filter secret scanning alerts to publicly leaked ones.",
372
+ )
373
+ parser.add_argument(
374
+ "--is-multi-repo",
375
+ action="store_true",
376
+ help="Filter secret scanning alerts to multi-repo ones.",
377
+ )
378
+ parser.add_argument(
379
+ "--sort",
380
+ default=None,
381
+ help="Surface-specific sort field.",
382
+ )
383
+ parser.add_argument(
384
+ "--direction",
385
+ default=None,
386
+ help="Surface-specific sort direction.",
387
+ )
388
+ parser.add_argument(
389
+ "--page",
390
+ type=int,
391
+ default=1,
392
+ help="Surface-specific page number for code or secret scanning selection.",
393
+ )
394
+ parser.add_argument(
395
+ "--per-page",
396
+ type=int,
397
+ default=DEFAULT_SUMMARY_PAGE_SIZE,
398
+ help="Maximum number of alerts to request during selection.",
399
+ )
400
+ parser.add_argument(
401
+ "--dry-run",
402
+ action="store_true",
403
+ help="Show the selected alerts and planned payload without mutating anything.",
404
+ )
405
+
406
+
407
+ def add_code_scanning_common_filters(parser: argparse.ArgumentParser) -> None:
408
+ parser.add_argument(
409
+ "--tool-name", default=None, help="Filter by tool name."
410
+ )
411
+ parser.add_argument(
412
+ "--tool-guid", default=None, help="Filter by tool GUID."
413
+ )
414
+ parser.add_argument("--state", default=None, help=HELP_ALERT_STATE_FILTER)
415
+ parser.add_argument("--severity", default=None, help="Severity filter.")
416
+ parser.add_argument("--assignees", default=None, help=HELP_ASSIGNEE_FILTER)
417
+ parser.add_argument("--ref", default=None, help="Git ref filter.")
418
+ parser.add_argument(
419
+ "--pr", type=int, default=None, help="Pull request number filter."
420
+ )
421
+ parser.add_argument("--sort", default=None, help=HELP_SORT_FIELD)
422
+ parser.add_argument("--direction", default=None, help=HELP_SORT_DIRECTION)
423
+ parser.add_argument("--page", type=int, default=1, help=HELP_PAGE_NUMBER)
424
+ parser.add_argument(
425
+ "--per-page",
426
+ type=int,
427
+ default=DEFAULT_PAGE_SIZE,
428
+ help=HELP_ALERTS_PER_PAGE,
429
+ )
430
+
431
+
432
+ def add_code_scanning_list_parser(
433
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
434
+ ) -> None:
435
+ parser = subparsers.add_parser(
436
+ "list-code-scanning",
437
+ help="List code scanning alerts for a repository.",
438
+ )
439
+ add_code_scanning_common_filters(parser)
440
+
441
+
442
+ def add_code_scanning_show_parser(
443
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
444
+ ) -> None:
445
+ parser = subparsers.add_parser(
446
+ "show-code-scanning",
447
+ help="Show one code scanning alert, optionally with instances and autofix status.",
448
+ )
449
+ parser.add_argument(
450
+ "--alert", required=True, type=int, help="Code scanning alert number."
451
+ )
452
+ parser.add_argument(
453
+ "--include-instances",
454
+ action="store_true",
455
+ help="Also fetch alert instances.",
456
+ )
457
+ parser.add_argument(
458
+ "--include-autofix",
459
+ action="store_true",
460
+ help="Also fetch autofix status.",
461
+ )
462
+ parser.add_argument(
463
+ "--instances-per-page",
464
+ type=int,
465
+ default=DEFAULT_PAGE_SIZE,
466
+ help="Instances page size when --include-instances is used.",
467
+ )
468
+
469
+
470
+ def add_code_scanning_update_parser(
471
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
472
+ ) -> None:
473
+ parser = subparsers.add_parser(
474
+ "update-code-scanning",
475
+ help="Dismiss, reopen, or reassign a code scanning alert.",
476
+ )
477
+ parser.add_argument(
478
+ "--alert", required=True, type=int, help="Code scanning alert number."
479
+ )
480
+ parser.add_argument(
481
+ "--state",
482
+ required=True,
483
+ choices=("open", "dismissed"),
484
+ help=HELP_DESIRED_ALERT_STATE,
485
+ )
486
+ parser.add_argument(
487
+ "--dismissed-reason",
488
+ default=None,
489
+ choices=CODE_SCANNING_DISMISS_REASONS,
490
+ help=HELP_DISMISSAL_REASON,
491
+ )
492
+ parser.add_argument(
493
+ "--comment", default=None, help=HELP_DISMISSAL_COMMENT
494
+ )
495
+ parser.add_argument(
496
+ "--assignee",
497
+ action="append",
498
+ dest="assignees",
499
+ default=None,
500
+ help=HELP_ASSIGNEE_REPEAT,
501
+ )
502
+ parser.add_argument(
503
+ "--clear-assignees",
504
+ action="store_true",
505
+ help=HELP_REMOVE_ASSIGNEES,
506
+ )
507
+ parser.add_argument(
508
+ "--create-request",
509
+ action="store_true",
510
+ help="Ask GitHub to create an alert dismissal request when supported.",
511
+ )
512
+ parser.add_argument(
513
+ "--dry-run",
514
+ action="store_true",
515
+ help=HELP_DRY_RUN,
516
+ )
517
+
518
+
519
+ def add_dependabot_common_filters(parser: argparse.ArgumentParser) -> None:
520
+ parser.add_argument("--state", default=None, help=HELP_ALERT_STATE_FILTER)
521
+ parser.add_argument("--severity", default=None, help="Severity filter.")
522
+ parser.add_argument("--ecosystem", default=None, help="Ecosystem filter.")
523
+ parser.add_argument("--package", default=None, help="Package-name filter.")
524
+ parser.add_argument(
525
+ "--manifest", default=None, help="Manifest-path filter."
526
+ )
527
+ parser.add_argument("--epss-percentage", default=None, help="EPSS filter.")
528
+ parser.add_argument(
529
+ "--has",
530
+ dest="has_filter",
531
+ default=None,
532
+ help="Has filter, for example patch.",
533
+ )
534
+ parser.add_argument("--assignee", default=None, help=HELP_ASSIGNEE_FILTER)
535
+ parser.add_argument(
536
+ "--scope", default=None, help="Dependency scope filter."
537
+ )
538
+ parser.add_argument("--sort", default=None, help=HELP_SORT_FIELD)
539
+ parser.add_argument("--direction", default=None, help=HELP_SORT_DIRECTION)
540
+ parser.add_argument(
541
+ "--before", default=None, help="Cursor for the previous page."
542
+ )
543
+ parser.add_argument(
544
+ "--after", default=None, help="Cursor for the next page."
545
+ )
546
+ parser.add_argument(
547
+ "--per-page",
548
+ type=int,
549
+ default=DEFAULT_PAGE_SIZE,
550
+ help=HELP_ALERTS_PER_PAGE,
551
+ )
552
+
553
+
554
+ def add_dependabot_list_parser(
555
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
556
+ ) -> None:
557
+ parser = subparsers.add_parser(
558
+ "list-dependabot",
559
+ help="List Dependabot alerts for a repository.",
560
+ )
561
+ add_dependabot_common_filters(parser)
562
+
563
+
564
+ def add_dependabot_show_parser(
565
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
566
+ ) -> None:
567
+ parser = subparsers.add_parser(
568
+ "show-dependabot", help="Show one Dependabot alert."
569
+ )
570
+ parser.add_argument(
571
+ "--alert", required=True, type=int, help="Dependabot alert number."
572
+ )
573
+
574
+
575
+ def add_dependabot_update_parser(
576
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
577
+ ) -> None:
578
+ parser = subparsers.add_parser(
579
+ "update-dependabot",
580
+ help="Dismiss, reopen, or reassign a Dependabot alert.",
581
+ )
582
+ parser.add_argument(
583
+ "--alert", required=True, type=int, help="Dependabot alert number."
584
+ )
585
+ parser.add_argument(
586
+ "--state",
587
+ required=True,
588
+ choices=("open", "dismissed"),
589
+ help=HELP_DESIRED_ALERT_STATE,
590
+ )
591
+ parser.add_argument(
592
+ "--dismissed-reason",
593
+ default=None,
594
+ choices=DEPENDABOT_DISMISS_REASONS,
595
+ help=HELP_DISMISSAL_REASON,
596
+ )
597
+ parser.add_argument(
598
+ "--comment", default=None, help=HELP_DISMISSAL_COMMENT
599
+ )
600
+ parser.add_argument(
601
+ "--assignee",
602
+ action="append",
603
+ dest="assignees",
604
+ default=None,
605
+ help=HELP_ASSIGNEE_REPEAT,
606
+ )
607
+ parser.add_argument(
608
+ "--clear-assignees",
609
+ action="store_true",
610
+ help=HELP_REMOVE_ASSIGNEES,
611
+ )
612
+ parser.add_argument(
613
+ "--dry-run",
614
+ action="store_true",
615
+ help=HELP_DRY_RUN,
616
+ )
617
+
618
+
619
+ def add_malware_list_parser(
620
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
621
+ ) -> None:
622
+ parser = subparsers.add_parser(
623
+ "list-malware",
624
+ help="List Dependabot malware alerts for a repository.",
625
+ )
626
+ add_dependabot_common_filters(parser)
627
+
628
+
629
+ def add_malware_show_parser(
630
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
631
+ ) -> None:
632
+ parser = subparsers.add_parser(
633
+ "show-malware",
634
+ help="Show one malware alert, backed by a Dependabot alert whose advisory type is malware.",
635
+ )
636
+ parser.add_argument(
637
+ "--alert", required=True, type=int, help="Alert number."
638
+ )
639
+
640
+
641
+ def add_malware_update_parser(
642
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
643
+ ) -> None:
644
+ parser = subparsers.add_parser(
645
+ "update-malware",
646
+ help="Dismiss, reopen, or reassign a malware alert via the Dependabot alert API.",
647
+ )
648
+ parser.add_argument(
649
+ "--alert", required=True, type=int, help="Alert number."
650
+ )
651
+ parser.add_argument(
652
+ "--state",
653
+ required=True,
654
+ choices=("open", "dismissed"),
655
+ help=HELP_DESIRED_ALERT_STATE,
656
+ )
657
+ parser.add_argument(
658
+ "--dismissed-reason",
659
+ default=None,
660
+ choices=DEPENDABOT_DISMISS_REASONS,
661
+ help=HELP_DISMISSAL_REASON,
662
+ )
663
+ parser.add_argument(
664
+ "--comment", default=None, help=HELP_DISMISSAL_COMMENT
665
+ )
666
+ parser.add_argument(
667
+ "--assignee",
668
+ action="append",
669
+ dest="assignees",
670
+ default=None,
671
+ help=HELP_ASSIGNEE_REPEAT,
672
+ )
673
+ parser.add_argument(
674
+ "--clear-assignees",
675
+ action="store_true",
676
+ help=HELP_REMOVE_ASSIGNEES,
677
+ )
678
+ parser.add_argument(
679
+ "--dry-run",
680
+ action="store_true",
681
+ help=HELP_DRY_RUN,
682
+ )
683
+ parser.add_argument(
684
+ "--skip-malware-check",
685
+ action="store_true",
686
+ help="Apply the update without verifying the alert maps to a malware advisory.",
687
+ )
688
+
689
+
690
+ def add_secret_scanning_common_filters(
691
+ parser: argparse.ArgumentParser,
692
+ ) -> None:
693
+ parser.add_argument("--state", default=None, help=HELP_ALERT_STATE_FILTER)
694
+ parser.add_argument(
695
+ "--secret-type", default=None, help="Secret type filter."
696
+ )
697
+ parser.add_argument(
698
+ "--resolution", default=None, help="Resolution filter."
699
+ )
700
+ parser.add_argument("--assignee", default=None, help=HELP_ASSIGNEE_FILTER)
701
+ parser.add_argument("--validity", default=None, help="Validity filter.")
702
+ parser.add_argument(
703
+ "--is-publicly-leaked",
704
+ action="store_true",
705
+ help="Filter to publicly leaked alerts.",
706
+ )
707
+ parser.add_argument(
708
+ "--is-multi-repo",
709
+ action="store_true",
710
+ help="Filter to multi-repo alerts.",
711
+ )
712
+ parser.add_argument("--sort", default=None, help=HELP_SORT_FIELD)
713
+ parser.add_argument("--direction", default=None, help=HELP_SORT_DIRECTION)
714
+ parser.add_argument("--page", type=int, default=1, help=HELP_PAGE_NUMBER)
715
+ parser.add_argument(
716
+ "--per-page",
717
+ type=int,
718
+ default=DEFAULT_PAGE_SIZE,
719
+ help=HELP_ALERTS_PER_PAGE,
720
+ )
721
+ parser.add_argument(
722
+ "--show-secret-values",
723
+ action="store_true",
724
+ help="Do not request secret redaction from the API.",
725
+ )
726
+
727
+
728
+ def add_secret_scanning_list_parser(
729
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
730
+ ) -> None:
731
+ parser = subparsers.add_parser(
732
+ "list-secret-scanning",
733
+ help="List secret scanning alerts for a repository.",
734
+ )
735
+ add_secret_scanning_common_filters(parser)
736
+
737
+
738
+ def add_secret_scanning_show_parser(
739
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
740
+ ) -> None:
741
+ parser = subparsers.add_parser(
742
+ "show-secret-scanning", help="Show one secret scanning alert."
743
+ )
744
+ parser.add_argument(
745
+ "--alert",
746
+ required=True,
747
+ type=int,
748
+ help=HELP_SECRET_ALERT_NUMBER,
749
+ )
750
+ parser.add_argument(
751
+ "--show-secret-values",
752
+ action="store_true",
753
+ help="Do not request secret redaction from the API.",
754
+ )
755
+
756
+
757
+ def add_secret_scanning_update_parser(
758
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
759
+ ) -> None:
760
+ parser = subparsers.add_parser(
761
+ "update-secret-scanning",
762
+ help="Resolve, reopen, or reassign a secret scanning alert.",
763
+ )
764
+ parser.add_argument(
765
+ "--alert",
766
+ required=True,
767
+ type=int,
768
+ help=HELP_SECRET_ALERT_NUMBER,
769
+ )
770
+ parser.add_argument(
771
+ "--state",
772
+ required=True,
773
+ choices=("open", "resolved"),
774
+ help=HELP_DESIRED_ALERT_STATE,
775
+ )
776
+ parser.add_argument(
777
+ "--resolution",
778
+ default=None,
779
+ choices=SECRET_SCANNING_RESOLUTIONS,
780
+ help="Resolution when state is resolved.",
781
+ )
782
+ parser.add_argument(
783
+ "--comment", default=None, help="Optional resolution comment."
784
+ )
785
+ parser.add_argument(
786
+ "--assignee",
787
+ default=None,
788
+ help="Assign the alert to this user login.",
789
+ )
790
+ parser.add_argument(
791
+ "--unassign",
792
+ action="store_true",
793
+ help="Remove the current assignee.",
794
+ )
795
+ parser.add_argument(
796
+ "--dry-run",
797
+ action="store_true",
798
+ help=HELP_DRY_RUN,
799
+ )
800
+
801
+
802
+ def add_secret_locations_parser(
803
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
804
+ ) -> None:
805
+ parser = subparsers.add_parser(
806
+ "list-secret-locations",
807
+ help="List all known locations for a secret scanning alert.",
808
+ )
809
+ parser.add_argument(
810
+ "--alert",
811
+ required=True,
812
+ type=int,
813
+ help=HELP_SECRET_ALERT_NUMBER,
814
+ )
815
+ parser.add_argument("--page", type=int, default=1, help=HELP_PAGE_NUMBER)
816
+ parser.add_argument(
817
+ "--per-page",
818
+ type=int,
819
+ default=DEFAULT_PAGE_SIZE,
820
+ help="Number of locations per page.",
821
+ )
822
+
823
+
824
+ def add_api_call_parser(
825
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
826
+ ) -> None:
827
+ parser = subparsers.add_parser(
828
+ "api-call",
829
+ help="Raw GitHub API fallback for anything not wrapped yet.",
830
+ )
831
+ parser.add_argument(
832
+ "--endpoint",
833
+ required=True,
834
+ help="API endpoint path, for example /repos/OWNER/REPO/code-scanning/default-setup.",
835
+ )
836
+ parser.add_argument("--method", default="GET", help="HTTP method.")
837
+ parser.add_argument(
838
+ "--query-param",
839
+ action="append",
840
+ dest="query_params",
841
+ default=None,
842
+ help="Query parameter in key=value form. Repeat for multiple parameters.",
843
+ )
844
+ parser.add_argument(
845
+ "--body-json",
846
+ default=None,
847
+ help="Optional JSON request body for POST, PATCH, or PUT operations.",
848
+ )