github-manage-security-alerts-skill 1.0.0 → 1.0.2
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.
- package/CONTRIBUTING.md +2 -2
- package/README.md +17 -20
- package/SKILL.md +233 -233
- package/package.json +13 -5
- package/scripts/github_security_api.py +360 -358
- package/scripts/github_security_cli.py +848 -835
- package/scripts/github_security_common.py +103 -103
- package/scripts/github_security_operations.py +1246 -1162
- package/scripts/github_security_render.py +310 -318
- package/scripts/manage_github_security_alerts.py +57 -58
|
@@ -1,1162 +1,1246 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from collections import Counter
|
|
5
|
-
from types import SimpleNamespace
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
from github_security_api import RepoContext, api_request, safe_api_request
|
|
9
|
-
from github_security_common import (
|
|
10
|
-
GitHubSecurityCliError,
|
|
11
|
-
expect_dict,
|
|
12
|
-
expect_list,
|
|
13
|
-
filter_non_null_values,
|
|
14
|
-
normalize_repeated_values,
|
|
15
|
-
parse_name_value_pairs,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if arguments.
|
|
162
|
-
payload["
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
payload["assignees"] =
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
"
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
payload["assignees"] =
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
"
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
ghsa_id
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
) ->
|
|
499
|
-
"""
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
)
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
"
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
def
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
"
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
context,
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
"
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
"
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
"
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
"
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
"
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
"
|
|
729
|
-
"
|
|
730
|
-
"
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
"
|
|
749
|
-
"
|
|
750
|
-
"
|
|
751
|
-
"
|
|
752
|
-
"
|
|
753
|
-
"
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
"
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
"
|
|
772
|
-
"
|
|
773
|
-
"
|
|
774
|
-
"
|
|
775
|
-
"
|
|
776
|
-
"
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
)
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
if
|
|
844
|
-
raise GitHubSecurityCliError(
|
|
845
|
-
"
|
|
846
|
-
)
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
if
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
)
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
)
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
if
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
)
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
return
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from github_security_api import RepoContext, api_request, safe_api_request
|
|
9
|
+
from github_security_common import (
|
|
10
|
+
GitHubSecurityCliError,
|
|
11
|
+
expect_dict,
|
|
12
|
+
expect_list,
|
|
13
|
+
filter_non_null_values,
|
|
14
|
+
normalize_repeated_values,
|
|
15
|
+
parse_name_value_pairs,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
DEPENDABOT_ALERTS_LABEL = "Dependabot alerts"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_code_scanning_query(arguments: Any) -> dict[str, Any]:
|
|
22
|
+
"""Build query parameters for code scanning list calls."""
|
|
23
|
+
|
|
24
|
+
return filter_non_null_values(
|
|
25
|
+
{
|
|
26
|
+
"tool_name": arguments.tool_name,
|
|
27
|
+
"tool_guid": arguments.tool_guid,
|
|
28
|
+
"state": arguments.state,
|
|
29
|
+
"severity": arguments.severity,
|
|
30
|
+
"assignees": arguments.assignees,
|
|
31
|
+
"ref": arguments.ref,
|
|
32
|
+
"pr": arguments.pr,
|
|
33
|
+
"sort": arguments.sort,
|
|
34
|
+
"direction": arguments.direction,
|
|
35
|
+
"page": arguments.page,
|
|
36
|
+
"per_page": arguments.per_page,
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_dependabot_query(arguments: Any) -> dict[str, Any]:
|
|
42
|
+
"""Build query parameters for Dependabot list calls."""
|
|
43
|
+
|
|
44
|
+
return filter_non_null_values(
|
|
45
|
+
{
|
|
46
|
+
"state": arguments.state,
|
|
47
|
+
"severity": arguments.severity,
|
|
48
|
+
"ecosystem": arguments.ecosystem,
|
|
49
|
+
"package": arguments.package,
|
|
50
|
+
"manifest": arguments.manifest,
|
|
51
|
+
"epss_percentage": arguments.epss_percentage,
|
|
52
|
+
"has": arguments.has_filter,
|
|
53
|
+
"assignee": arguments.assignee,
|
|
54
|
+
"scope": arguments.scope,
|
|
55
|
+
"sort": arguments.sort,
|
|
56
|
+
"direction": arguments.direction,
|
|
57
|
+
"before": arguments.before,
|
|
58
|
+
"after": arguments.after,
|
|
59
|
+
"per_page": arguments.per_page,
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def build_secret_scanning_query(arguments: Any) -> dict[str, Any]:
|
|
65
|
+
"""Build query parameters for secret scanning list calls."""
|
|
66
|
+
|
|
67
|
+
return filter_non_null_values(
|
|
68
|
+
{
|
|
69
|
+
"state": arguments.state,
|
|
70
|
+
"secret_type": arguments.secret_type,
|
|
71
|
+
"resolution": arguments.resolution,
|
|
72
|
+
"assignee": arguments.assignee,
|
|
73
|
+
"validity": arguments.validity,
|
|
74
|
+
"is_publicly_leaked": (
|
|
75
|
+
True if arguments.is_publicly_leaked else None
|
|
76
|
+
),
|
|
77
|
+
"is_multi_repo": True if arguments.is_multi_repo else None,
|
|
78
|
+
"hide_secret": False if arguments.show_secret_values else True,
|
|
79
|
+
"sort": arguments.sort,
|
|
80
|
+
"direction": arguments.direction,
|
|
81
|
+
"page": arguments.page,
|
|
82
|
+
"per_page": arguments.per_page,
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def fetch_code_scanning_alerts(
|
|
88
|
+
context: RepoContext, query: dict[str, Any]
|
|
89
|
+
) -> list[dict[str, Any]]:
|
|
90
|
+
"""List code scanning alerts."""
|
|
91
|
+
|
|
92
|
+
response = api_request(
|
|
93
|
+
context,
|
|
94
|
+
endpoint=f"/repos/{context.owner}/{context.repo}/code-scanning/alerts",
|
|
95
|
+
params=query,
|
|
96
|
+
)
|
|
97
|
+
return expect_list(response.data, "code scanning alerts")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def fetch_code_scanning_alert(
|
|
101
|
+
context: RepoContext, alert_number: int
|
|
102
|
+
) -> dict[str, Any]:
|
|
103
|
+
"""Fetch one code scanning alert."""
|
|
104
|
+
|
|
105
|
+
response = api_request(
|
|
106
|
+
context,
|
|
107
|
+
endpoint=(
|
|
108
|
+
f"/repos/{context.owner}/{context.repo}/code-scanning/alerts/{alert_number}"
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
return expect_dict(response.data, "code scanning alert")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def fetch_code_scanning_instances(
|
|
115
|
+
context: RepoContext,
|
|
116
|
+
alert_number: int,
|
|
117
|
+
*,
|
|
118
|
+
page: int,
|
|
119
|
+
per_page: int,
|
|
120
|
+
) -> list[dict[str, Any]]:
|
|
121
|
+
"""Fetch alert instances for one code scanning alert."""
|
|
122
|
+
|
|
123
|
+
response = api_request(
|
|
124
|
+
context,
|
|
125
|
+
endpoint=(
|
|
126
|
+
f"/repos/{context.owner}/{context.repo}/code-scanning/alerts/{alert_number}/instances"
|
|
127
|
+
),
|
|
128
|
+
params={"page": page, "per_page": per_page},
|
|
129
|
+
)
|
|
130
|
+
return expect_list(response.data, "code scanning instances")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def fetch_code_scanning_autofix(
|
|
134
|
+
context: RepoContext, alert_number: int
|
|
135
|
+
) -> dict[str, Any] | None:
|
|
136
|
+
"""Fetch code scanning autofix status when available."""
|
|
137
|
+
|
|
138
|
+
result = safe_api_request(
|
|
139
|
+
context,
|
|
140
|
+
endpoint=(
|
|
141
|
+
f"/repos/{context.owner}/{context.repo}/code-scanning/alerts/{alert_number}/autofix"
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
if not result["ok"]:
|
|
145
|
+
return {"error": result["error"]}
|
|
146
|
+
return expect_dict(result["data"], "code scanning autofix status")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def build_code_scanning_update_payload(arguments: Any) -> dict[str, Any]:
|
|
150
|
+
"""Build a code scanning alert update payload."""
|
|
151
|
+
|
|
152
|
+
payload: dict[str, Any] = {"state": arguments.state}
|
|
153
|
+
assignees = normalize_repeated_values(arguments.assignees)
|
|
154
|
+
|
|
155
|
+
if arguments.state == "dismissed":
|
|
156
|
+
if arguments.dismissed_reason is None:
|
|
157
|
+
raise GitHubSecurityCliError(
|
|
158
|
+
"--dismissed-reason is required when dismissing a code scanning alert."
|
|
159
|
+
)
|
|
160
|
+
payload["dismissed_reason"] = arguments.dismissed_reason
|
|
161
|
+
if arguments.comment is not None:
|
|
162
|
+
payload["dismissed_comment"] = arguments.comment
|
|
163
|
+
if arguments.create_request:
|
|
164
|
+
payload["create_request"] = True
|
|
165
|
+
else:
|
|
166
|
+
if arguments.dismissed_reason is not None:
|
|
167
|
+
raise GitHubSecurityCliError(
|
|
168
|
+
"--dismissed-reason can only be used when state is dismissed."
|
|
169
|
+
)
|
|
170
|
+
if arguments.comment is not None:
|
|
171
|
+
raise GitHubSecurityCliError(
|
|
172
|
+
"--comment can only be used when state is dismissed for code scanning alerts."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if arguments.clear_assignees:
|
|
176
|
+
payload["assignees"] = []
|
|
177
|
+
elif assignees:
|
|
178
|
+
payload["assignees"] = assignees
|
|
179
|
+
|
|
180
|
+
return payload
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def update_code_scanning_alert(
|
|
184
|
+
context: RepoContext, arguments: Any
|
|
185
|
+
) -> dict[str, Any]:
|
|
186
|
+
"""Dismiss, reopen, or reassign a code scanning alert."""
|
|
187
|
+
|
|
188
|
+
payload = build_code_scanning_update_payload(arguments)
|
|
189
|
+
if arguments.dry_run:
|
|
190
|
+
return {
|
|
191
|
+
"dry_run": True,
|
|
192
|
+
"endpoint": f"/repos/{context.owner}/{context.repo}/code-scanning/alerts/{arguments.alert}",
|
|
193
|
+
"payload": payload,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
response = api_request(
|
|
197
|
+
context,
|
|
198
|
+
endpoint=(
|
|
199
|
+
f"/repos/{context.owner}/{context.repo}/code-scanning/alerts/{arguments.alert}"
|
|
200
|
+
),
|
|
201
|
+
method="PATCH",
|
|
202
|
+
body=payload,
|
|
203
|
+
)
|
|
204
|
+
return expect_dict(response.data, "updated code scanning alert")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def fetch_dependabot_alerts(
|
|
208
|
+
context: RepoContext, query: dict[str, Any]
|
|
209
|
+
) -> list[dict[str, Any]]:
|
|
210
|
+
"""List Dependabot alerts."""
|
|
211
|
+
|
|
212
|
+
response = api_request(
|
|
213
|
+
context,
|
|
214
|
+
endpoint=f"/repos/{context.owner}/{context.repo}/dependabot/alerts",
|
|
215
|
+
params=query,
|
|
216
|
+
)
|
|
217
|
+
return expect_list(response.data, DEPENDABOT_ALERTS_LABEL)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def fetch_dependabot_alert(
|
|
221
|
+
context: RepoContext, alert_number: int
|
|
222
|
+
) -> dict[str, Any]:
|
|
223
|
+
"""Fetch one Dependabot alert."""
|
|
224
|
+
|
|
225
|
+
response = api_request(
|
|
226
|
+
context,
|
|
227
|
+
endpoint=f"/repos/{context.owner}/{context.repo}/dependabot/alerts/{alert_number}",
|
|
228
|
+
)
|
|
229
|
+
return expect_dict(response.data, "Dependabot alert")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def build_dependabot_update_payload(arguments: Any) -> dict[str, Any]:
|
|
233
|
+
"""Build a Dependabot alert update payload."""
|
|
234
|
+
|
|
235
|
+
payload: dict[str, Any] = {"state": arguments.state}
|
|
236
|
+
assignees = normalize_repeated_values(arguments.assignees)
|
|
237
|
+
|
|
238
|
+
if arguments.state == "dismissed":
|
|
239
|
+
if arguments.dismissed_reason is None:
|
|
240
|
+
raise GitHubSecurityCliError(
|
|
241
|
+
"--dismissed-reason is required when dismissing a Dependabot alert."
|
|
242
|
+
)
|
|
243
|
+
payload["dismissed_reason"] = arguments.dismissed_reason
|
|
244
|
+
if arguments.comment is not None:
|
|
245
|
+
payload["dismissed_comment"] = arguments.comment
|
|
246
|
+
else:
|
|
247
|
+
if arguments.dismissed_reason is not None:
|
|
248
|
+
raise GitHubSecurityCliError(
|
|
249
|
+
"--dismissed-reason can only be used when state is dismissed."
|
|
250
|
+
)
|
|
251
|
+
if arguments.comment is not None:
|
|
252
|
+
raise GitHubSecurityCliError(
|
|
253
|
+
"--comment can only be used when state is dismissed for Dependabot alerts."
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if arguments.clear_assignees:
|
|
257
|
+
payload["assignees"] = []
|
|
258
|
+
elif assignees:
|
|
259
|
+
payload["assignees"] = assignees
|
|
260
|
+
|
|
261
|
+
return payload
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def update_dependabot_alert(
|
|
265
|
+
context: RepoContext, arguments: Any
|
|
266
|
+
) -> dict[str, Any]:
|
|
267
|
+
"""Dismiss, reopen, or reassign a Dependabot alert."""
|
|
268
|
+
|
|
269
|
+
payload = build_dependabot_update_payload(arguments)
|
|
270
|
+
if arguments.dry_run:
|
|
271
|
+
return {
|
|
272
|
+
"dry_run": True,
|
|
273
|
+
"endpoint": f"/repos/{context.owner}/{context.repo}/dependabot/alerts/{arguments.alert}",
|
|
274
|
+
"payload": payload,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
response = api_request(
|
|
278
|
+
context,
|
|
279
|
+
endpoint=f"/repos/{context.owner}/{context.repo}/dependabot/alerts/{arguments.alert}",
|
|
280
|
+
method="PATCH",
|
|
281
|
+
body=payload,
|
|
282
|
+
)
|
|
283
|
+
return expect_dict(response.data, "updated Dependabot alert")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def fetch_global_advisory_type(
|
|
287
|
+
context: RepoContext,
|
|
288
|
+
ghsa_id: str,
|
|
289
|
+
advisory_type_cache: dict[str, dict[str, Any] | None],
|
|
290
|
+
) -> dict[str, Any] | None:
|
|
291
|
+
"""Fetch one global advisory so malware alerts can be classified."""
|
|
292
|
+
|
|
293
|
+
cached_result = advisory_type_cache.get(ghsa_id)
|
|
294
|
+
if cached_result is not None or ghsa_id in advisory_type_cache:
|
|
295
|
+
return cached_result
|
|
296
|
+
|
|
297
|
+
result = safe_api_request(context, endpoint=f"/advisories/{ghsa_id}")
|
|
298
|
+
if not result["ok"]:
|
|
299
|
+
advisory_type_cache[ghsa_id] = {
|
|
300
|
+
"error": result["error"],
|
|
301
|
+
"ghsa_id": ghsa_id,
|
|
302
|
+
}
|
|
303
|
+
return advisory_type_cache[ghsa_id]
|
|
304
|
+
|
|
305
|
+
advisory = expect_dict(result["data"], f"advisory {ghsa_id}")
|
|
306
|
+
advisory_type_cache[ghsa_id] = advisory
|
|
307
|
+
return advisory
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def get_alert_ghsa_id(alert: dict[str, Any]) -> str | None:
|
|
311
|
+
"""Extract a GHSA identifier from a Dependabot alert payload."""
|
|
312
|
+
|
|
313
|
+
advisory = alert.get("security_advisory")
|
|
314
|
+
if not isinstance(advisory, dict):
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
direct_ghsa_id = non_empty_string(advisory.get("ghsa_id"))
|
|
318
|
+
if direct_ghsa_id is not None:
|
|
319
|
+
return direct_ghsa_id
|
|
320
|
+
|
|
321
|
+
return ghsa_id_from_identifiers(advisory.get("identifiers"))
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def non_empty_string(value: Any) -> str | None:
|
|
325
|
+
if isinstance(value, str) and value.strip():
|
|
326
|
+
return value.strip()
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def ghsa_id_from_identifiers(identifiers: Any) -> str | None:
|
|
331
|
+
if not isinstance(identifiers, list):
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
for identifier in identifiers:
|
|
335
|
+
ghsa_id = ghsa_id_from_identifier(identifier)
|
|
336
|
+
if ghsa_id is not None:
|
|
337
|
+
return ghsa_id
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def ghsa_id_from_identifier(identifier: Any) -> str | None:
|
|
342
|
+
if not isinstance(identifier, dict):
|
|
343
|
+
return None
|
|
344
|
+
if identifier.get("type") != "GHSA":
|
|
345
|
+
return None
|
|
346
|
+
return non_empty_string(identifier.get("value"))
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def classify_malware_alerts(
|
|
350
|
+
context: RepoContext,
|
|
351
|
+
alerts: list[dict[str, Any]],
|
|
352
|
+
advisory_cache: dict[str, dict[str, Any] | None] | None = None,
|
|
353
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
354
|
+
"""Filter Dependabot alerts to those backed by malware advisories."""
|
|
355
|
+
|
|
356
|
+
malware_alerts: list[dict[str, Any]] = []
|
|
357
|
+
lookup_failures: list[dict[str, Any]] = []
|
|
358
|
+
cache = advisory_cache if advisory_cache is not None else {}
|
|
359
|
+
|
|
360
|
+
for alert in alerts:
|
|
361
|
+
classified = classify_one_malware_alert(context, alert, cache)
|
|
362
|
+
if classified is None:
|
|
363
|
+
continue
|
|
364
|
+
if "error" in classified:
|
|
365
|
+
lookup_failures.append(classified)
|
|
366
|
+
else:
|
|
367
|
+
malware_alerts.append(classified)
|
|
368
|
+
|
|
369
|
+
return malware_alerts, lookup_failures
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def classify_one_malware_alert(
|
|
373
|
+
context: RepoContext,
|
|
374
|
+
alert: dict[str, Any],
|
|
375
|
+
advisory_cache: dict[str, dict[str, Any] | None],
|
|
376
|
+
) -> dict[str, Any] | None:
|
|
377
|
+
ghsa_id = get_alert_ghsa_id(alert)
|
|
378
|
+
if ghsa_id is None:
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
advisory = fetch_global_advisory_type(context, ghsa_id, advisory_cache)
|
|
382
|
+
if advisory is None:
|
|
383
|
+
return None
|
|
384
|
+
if "error" in advisory:
|
|
385
|
+
return {
|
|
386
|
+
"alert_number": alert.get("number"),
|
|
387
|
+
"ghsa_id": ghsa_id,
|
|
388
|
+
"error": advisory["error"],
|
|
389
|
+
}
|
|
390
|
+
if advisory.get("type") != "malware":
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
malware_alert = dict(alert)
|
|
394
|
+
malware_alert["malware_advisory"] = advisory
|
|
395
|
+
return malware_alert
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def maybe_raise_if_not_malware(
|
|
399
|
+
context: RepoContext,
|
|
400
|
+
alert_number: int,
|
|
401
|
+
advisory_cache: dict[str, dict[str, Any] | None],
|
|
402
|
+
) -> dict[str, Any]:
|
|
403
|
+
"""Ensure that one Dependabot alert is backed by a malware advisory."""
|
|
404
|
+
|
|
405
|
+
alert = fetch_dependabot_alert(context, alert_number)
|
|
406
|
+
ghsa_id = get_alert_ghsa_id(alert)
|
|
407
|
+
if ghsa_id is None:
|
|
408
|
+
raise GitHubSecurityCliError(
|
|
409
|
+
"Dependabot alert "
|
|
410
|
+
f"{alert_number} does not expose a GHSA identifier, so it "
|
|
411
|
+
"cannot be confirmed as a malware alert."
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
advisory = fetch_global_advisory_type(context, ghsa_id, advisory_cache)
|
|
415
|
+
if advisory is None or "error" in advisory:
|
|
416
|
+
raise GitHubSecurityCliError(
|
|
417
|
+
f"Could not verify malware advisory type for alert {alert_number} (GHSA {ghsa_id})."
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
if advisory.get("type") != "malware":
|
|
421
|
+
raise GitHubSecurityCliError(
|
|
422
|
+
"Dependabot alert "
|
|
423
|
+
f"{alert_number} is not backed by a malware advisory "
|
|
424
|
+
f"(type={advisory.get('type')})."
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
enriched_alert = dict(alert)
|
|
428
|
+
enriched_alert["malware_advisory"] = advisory
|
|
429
|
+
return enriched_alert
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def fetch_secret_scanning_alerts(
|
|
433
|
+
context: RepoContext, query: dict[str, Any]
|
|
434
|
+
) -> list[dict[str, Any]]:
|
|
435
|
+
"""List secret scanning alerts."""
|
|
436
|
+
|
|
437
|
+
response = api_request(
|
|
438
|
+
context,
|
|
439
|
+
endpoint=f"/repos/{context.owner}/{context.repo}/secret-scanning/alerts",
|
|
440
|
+
params=query,
|
|
441
|
+
)
|
|
442
|
+
return expect_list(response.data, "secret scanning alerts")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def fetch_secret_scanning_alert(
|
|
446
|
+
context: RepoContext,
|
|
447
|
+
*,
|
|
448
|
+
alert_number: int,
|
|
449
|
+
show_secret_values: bool,
|
|
450
|
+
) -> dict[str, Any]:
|
|
451
|
+
"""Fetch one secret scanning alert."""
|
|
452
|
+
|
|
453
|
+
response = api_request(
|
|
454
|
+
context,
|
|
455
|
+
endpoint=(
|
|
456
|
+
f"/repos/{context.owner}/{context.repo}/secret-scanning/alerts/{alert_number}"
|
|
457
|
+
),
|
|
458
|
+
params={"hide_secret": False if show_secret_values else True},
|
|
459
|
+
)
|
|
460
|
+
return expect_dict(response.data, "secret scanning alert")
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def build_secret_scanning_update_payload(arguments: Any) -> dict[str, Any]:
|
|
464
|
+
"""Build a secret scanning alert update payload."""
|
|
465
|
+
|
|
466
|
+
payload: dict[str, Any] = {"state": arguments.state}
|
|
467
|
+
|
|
468
|
+
if arguments.state == "resolved":
|
|
469
|
+
if arguments.resolution is None:
|
|
470
|
+
raise GitHubSecurityCliError(
|
|
471
|
+
"--resolution is required when resolving a secret scanning alert."
|
|
472
|
+
)
|
|
473
|
+
payload["resolution"] = arguments.resolution
|
|
474
|
+
if arguments.comment is not None:
|
|
475
|
+
payload["resolution_comment"] = arguments.comment
|
|
476
|
+
else:
|
|
477
|
+
if arguments.resolution is not None:
|
|
478
|
+
raise GitHubSecurityCliError(
|
|
479
|
+
"--resolution can only be used when state is resolved."
|
|
480
|
+
)
|
|
481
|
+
if arguments.comment is not None:
|
|
482
|
+
payload["resolution_comment"] = arguments.comment
|
|
483
|
+
|
|
484
|
+
if arguments.unassign:
|
|
485
|
+
if arguments.assignee is not None:
|
|
486
|
+
raise GitHubSecurityCliError(
|
|
487
|
+
"Use either --assignee or --unassign, but not both."
|
|
488
|
+
)
|
|
489
|
+
payload["assignee"] = None
|
|
490
|
+
elif arguments.assignee is not None:
|
|
491
|
+
payload["assignee"] = arguments.assignee
|
|
492
|
+
|
|
493
|
+
return payload
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def update_secret_scanning_alert(
|
|
497
|
+
context: RepoContext, arguments: Any
|
|
498
|
+
) -> dict[str, Any]:
|
|
499
|
+
"""Resolve, reopen, or reassign a secret scanning alert."""
|
|
500
|
+
|
|
501
|
+
payload = build_secret_scanning_update_payload(arguments)
|
|
502
|
+
if arguments.dry_run:
|
|
503
|
+
return {
|
|
504
|
+
"dry_run": True,
|
|
505
|
+
"endpoint": f"/repos/{context.owner}/{context.repo}/secret-scanning/alerts/{arguments.alert}",
|
|
506
|
+
"payload": payload,
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
response = api_request(
|
|
510
|
+
context,
|
|
511
|
+
endpoint=(
|
|
512
|
+
f"/repos/{context.owner}/{context.repo}/secret-scanning/alerts/{arguments.alert}"
|
|
513
|
+
),
|
|
514
|
+
method="PATCH",
|
|
515
|
+
body=payload,
|
|
516
|
+
)
|
|
517
|
+
return expect_dict(response.data, "updated secret scanning alert")
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def fetch_secret_locations(
|
|
521
|
+
context: RepoContext,
|
|
522
|
+
*,
|
|
523
|
+
alert_number: int,
|
|
524
|
+
page: int,
|
|
525
|
+
per_page: int,
|
|
526
|
+
) -> list[dict[str, Any]]:
|
|
527
|
+
"""Fetch secret scanning locations for one alert."""
|
|
528
|
+
|
|
529
|
+
response = api_request(
|
|
530
|
+
context,
|
|
531
|
+
endpoint=(
|
|
532
|
+
f"/repos/{context.owner}/{context.repo}/secret-scanning/alerts/{alert_number}/locations"
|
|
533
|
+
),
|
|
534
|
+
params={"page": page, "per_page": per_page},
|
|
535
|
+
)
|
|
536
|
+
return expect_list(response.data, "secret scanning locations")
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def fetch_secret_scan_history(context: RepoContext) -> dict[str, Any]:
|
|
540
|
+
"""Fetch the secret scanning scan history for the repository."""
|
|
541
|
+
|
|
542
|
+
response = api_request(
|
|
543
|
+
context,
|
|
544
|
+
endpoint=f"/repos/{context.owner}/{context.repo}/secret-scanning/scan-history",
|
|
545
|
+
)
|
|
546
|
+
return expect_dict(response.data, "secret scanning scan history")
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def fetch_repository_overview(context: RepoContext) -> dict[str, Any]:
|
|
550
|
+
"""Fetch repository metadata and security_and_analysis settings."""
|
|
551
|
+
|
|
552
|
+
response = api_request(
|
|
553
|
+
context, endpoint=f"/repos/{context.owner}/{context.repo}"
|
|
554
|
+
)
|
|
555
|
+
repository = expect_dict(response.data, "repository overview")
|
|
556
|
+
return {
|
|
557
|
+
"api_base_url": context.api_base_url,
|
|
558
|
+
"full_name": context.full_name,
|
|
559
|
+
"html_url": repository.get("html_url"),
|
|
560
|
+
"private": repository.get("private"),
|
|
561
|
+
"security_and_analysis": repository.get("security_and_analysis"),
|
|
562
|
+
"visibility": repository.get("visibility"),
|
|
563
|
+
"web_base_url": context.web_base_url,
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def summarize_alert_collection(
|
|
568
|
+
result: dict[str, Any], *, sample_size: int, summary_kind: str
|
|
569
|
+
) -> dict[str, Any]:
|
|
570
|
+
"""Convert a safe list-call result into a compact summary payload."""
|
|
571
|
+
|
|
572
|
+
if not result["ok"]:
|
|
573
|
+
return {"error": result["error"], "ok": False}
|
|
574
|
+
|
|
575
|
+
alerts = expect_list(result["data"], f"{summary_kind} alerts")
|
|
576
|
+
counts_by_state = Counter(
|
|
577
|
+
str(alert.get("state", "unknown")) for alert in alerts
|
|
578
|
+
)
|
|
579
|
+
return {
|
|
580
|
+
"counts_by_state": dict(counts_by_state),
|
|
581
|
+
"ok": True,
|
|
582
|
+
"sample_alerts": alerts[:sample_size],
|
|
583
|
+
"total": len(alerts),
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def build_summary(context: RepoContext, arguments: Any) -> dict[str, Any]:
|
|
588
|
+
"""Build a cross-surface security summary for the repository."""
|
|
589
|
+
|
|
590
|
+
code_scanning_result = safe_api_request(
|
|
591
|
+
context,
|
|
592
|
+
endpoint=f"/repos/{context.owner}/{context.repo}/code-scanning/alerts",
|
|
593
|
+
params={"page": 1, "per_page": arguments.per_page},
|
|
594
|
+
)
|
|
595
|
+
dependabot_result = safe_api_request(
|
|
596
|
+
context,
|
|
597
|
+
endpoint=f"/repos/{context.owner}/{context.repo}/dependabot/alerts",
|
|
598
|
+
params={"per_page": arguments.per_page},
|
|
599
|
+
)
|
|
600
|
+
secret_scanning_result = safe_api_request(
|
|
601
|
+
context,
|
|
602
|
+
endpoint=f"/repos/{context.owner}/{context.repo}/secret-scanning/alerts",
|
|
603
|
+
params={
|
|
604
|
+
"page": 1,
|
|
605
|
+
"per_page": arguments.per_page,
|
|
606
|
+
"hide_secret": True,
|
|
607
|
+
},
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
advisory_cache: dict[str, dict[str, Any] | None] = {}
|
|
611
|
+
|
|
612
|
+
summary_sections: dict[str, Any] = {
|
|
613
|
+
"code_scanning": summarize_alert_collection(
|
|
614
|
+
code_scanning_result,
|
|
615
|
+
sample_size=arguments.sample_size,
|
|
616
|
+
summary_kind="code_scanning",
|
|
617
|
+
),
|
|
618
|
+
"dependabot": summarize_alert_collection(
|
|
619
|
+
dependabot_result,
|
|
620
|
+
sample_size=arguments.sample_size,
|
|
621
|
+
summary_kind="dependabot",
|
|
622
|
+
),
|
|
623
|
+
"secret_scanning": summarize_alert_collection(
|
|
624
|
+
secret_scanning_result,
|
|
625
|
+
sample_size=arguments.sample_size,
|
|
626
|
+
summary_kind="secret_scanning",
|
|
627
|
+
),
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
summary: dict[str, Any] = {
|
|
631
|
+
"api_base_url": context.api_base_url,
|
|
632
|
+
"full_name": context.full_name,
|
|
633
|
+
"repository_html_url": f"{context.web_base_url}/{context.full_name}",
|
|
634
|
+
"token_env": context.token_env_name,
|
|
635
|
+
"sections": summary_sections,
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if dependabot_result["ok"]:
|
|
639
|
+
dependabot_alerts = expect_list(
|
|
640
|
+
dependabot_result["data"], DEPENDABOT_ALERTS_LABEL
|
|
641
|
+
)
|
|
642
|
+
malware_alerts, lookup_failures = classify_malware_alerts(
|
|
643
|
+
context,
|
|
644
|
+
dependabot_alerts,
|
|
645
|
+
advisory_cache,
|
|
646
|
+
)
|
|
647
|
+
summary_sections["malware"] = {
|
|
648
|
+
"counts_by_state": dict(
|
|
649
|
+
Counter(
|
|
650
|
+
str(alert.get("state", "unknown"))
|
|
651
|
+
for alert in malware_alerts
|
|
652
|
+
)
|
|
653
|
+
),
|
|
654
|
+
"lookup_failures": lookup_failures,
|
|
655
|
+
"ok": True,
|
|
656
|
+
"sample_alerts": malware_alerts[: arguments.sample_size],
|
|
657
|
+
"total": len(malware_alerts),
|
|
658
|
+
}
|
|
659
|
+
else:
|
|
660
|
+
summary_sections["malware"] = {
|
|
661
|
+
"depends_on": "dependabot",
|
|
662
|
+
"error": dependabot_result["error"],
|
|
663
|
+
"ok": False,
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return summary
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def build_export_alerts(
|
|
670
|
+
context: RepoContext, arguments: Any
|
|
671
|
+
) -> dict[str, Any]:
|
|
672
|
+
"""Export full alert collections for bulk triage workflows."""
|
|
673
|
+
|
|
674
|
+
overview = fetch_repository_overview(context)
|
|
675
|
+
code_scanning_result = safe_api_request(
|
|
676
|
+
context,
|
|
677
|
+
endpoint=f"/repos/{context.owner}/{context.repo}/code-scanning/alerts",
|
|
678
|
+
params={
|
|
679
|
+
"page": 1,
|
|
680
|
+
"per_page": arguments.per_page,
|
|
681
|
+
"state": arguments.code_scanning_state,
|
|
682
|
+
},
|
|
683
|
+
)
|
|
684
|
+
dependabot_result = safe_api_request(
|
|
685
|
+
context,
|
|
686
|
+
endpoint=f"/repos/{context.owner}/{context.repo}/dependabot/alerts",
|
|
687
|
+
params={
|
|
688
|
+
"per_page": arguments.per_page,
|
|
689
|
+
"state": arguments.dependabot_state,
|
|
690
|
+
},
|
|
691
|
+
)
|
|
692
|
+
secret_scanning_result = safe_api_request(
|
|
693
|
+
context,
|
|
694
|
+
endpoint=f"/repos/{context.owner}/{context.repo}/secret-scanning/alerts",
|
|
695
|
+
params={
|
|
696
|
+
"page": 1,
|
|
697
|
+
"per_page": arguments.per_page,
|
|
698
|
+
"state": arguments.secret_scanning_state,
|
|
699
|
+
"hide_secret": False if arguments.show_secret_values else True,
|
|
700
|
+
},
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
export_sections: dict[str, Any] = {
|
|
704
|
+
"code_scanning": code_scanning_result,
|
|
705
|
+
"dependabot": dependabot_result,
|
|
706
|
+
"secret_scanning": secret_scanning_result,
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
export_bundle: dict[str, Any] = {
|
|
710
|
+
"api_base_url": context.api_base_url,
|
|
711
|
+
"full_name": context.full_name,
|
|
712
|
+
"repository": overview,
|
|
713
|
+
"sections": export_sections,
|
|
714
|
+
"token_env": context.token_env_name,
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if dependabot_result["ok"]:
|
|
718
|
+
advisories_cache: dict[str, dict[str, Any] | None] = {}
|
|
719
|
+
dependabot_alerts = expect_list(
|
|
720
|
+
dependabot_result["data"], DEPENDABOT_ALERTS_LABEL
|
|
721
|
+
)
|
|
722
|
+
malware_alerts, lookup_failures = classify_malware_alerts(
|
|
723
|
+
context,
|
|
724
|
+
dependabot_alerts,
|
|
725
|
+
advisories_cache,
|
|
726
|
+
)
|
|
727
|
+
export_sections["malware"] = {
|
|
728
|
+
"alerts": malware_alerts,
|
|
729
|
+
"lookup_failures": lookup_failures,
|
|
730
|
+
"ok": True,
|
|
731
|
+
"total": len(malware_alerts),
|
|
732
|
+
}
|
|
733
|
+
else:
|
|
734
|
+
export_sections["malware"] = {
|
|
735
|
+
"depends_on": "dependabot",
|
|
736
|
+
"error": dependabot_result["error"],
|
|
737
|
+
"ok": False,
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return export_bundle
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def build_bulk_code_scanning_query(arguments: Any) -> dict[str, Any]:
|
|
744
|
+
"""Build selection query parameters for bulk code-scanning updates."""
|
|
745
|
+
|
|
746
|
+
return filter_non_null_values(
|
|
747
|
+
{
|
|
748
|
+
"tool_name": arguments.tool_name,
|
|
749
|
+
"tool_guid": arguments.tool_guid,
|
|
750
|
+
"state": arguments.select_state,
|
|
751
|
+
"severity": arguments.severity,
|
|
752
|
+
"assignees": arguments.assignee_filter,
|
|
753
|
+
"ref": arguments.ref,
|
|
754
|
+
"pr": arguments.pr,
|
|
755
|
+
"sort": arguments.sort,
|
|
756
|
+
"direction": arguments.direction,
|
|
757
|
+
"page": arguments.page,
|
|
758
|
+
"per_page": arguments.per_page,
|
|
759
|
+
}
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def build_bulk_dependabot_query(arguments: Any) -> dict[str, Any]:
|
|
764
|
+
"""Build selection query parameters for bulk Dependabot updates."""
|
|
765
|
+
|
|
766
|
+
return filter_non_null_values(
|
|
767
|
+
{
|
|
768
|
+
"state": arguments.select_state,
|
|
769
|
+
"severity": arguments.severity,
|
|
770
|
+
"ecosystem": arguments.ecosystem,
|
|
771
|
+
"package": arguments.package,
|
|
772
|
+
"manifest": arguments.manifest,
|
|
773
|
+
"epss_percentage": arguments.epss_percentage,
|
|
774
|
+
"has": arguments.has_filter,
|
|
775
|
+
"assignee": arguments.assignee_filter,
|
|
776
|
+
"scope": arguments.scope,
|
|
777
|
+
"sort": arguments.sort,
|
|
778
|
+
"direction": arguments.direction,
|
|
779
|
+
"before": arguments.before,
|
|
780
|
+
"after": arguments.after,
|
|
781
|
+
"per_page": arguments.per_page,
|
|
782
|
+
}
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def build_bulk_secret_scanning_query(arguments: Any) -> dict[str, Any]:
|
|
787
|
+
"""Build selection query parameters for bulk secret-scanning updates."""
|
|
788
|
+
|
|
789
|
+
return filter_non_null_values(
|
|
790
|
+
{
|
|
791
|
+
"state": arguments.select_state,
|
|
792
|
+
"secret_type": arguments.secret_type,
|
|
793
|
+
"resolution": arguments.resolution_filter,
|
|
794
|
+
"assignee": arguments.assignee_filter,
|
|
795
|
+
"validity": arguments.validity,
|
|
796
|
+
"is_publicly_leaked": (
|
|
797
|
+
True if arguments.is_publicly_leaked else None
|
|
798
|
+
),
|
|
799
|
+
"is_multi_repo": True if arguments.is_multi_repo else None,
|
|
800
|
+
"hide_secret": False if arguments.show_secret_values else True,
|
|
801
|
+
"sort": arguments.sort,
|
|
802
|
+
"direction": arguments.direction,
|
|
803
|
+
"page": arguments.page,
|
|
804
|
+
"per_page": arguments.per_page,
|
|
805
|
+
}
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def get_bulk_mutation_target_state(
|
|
810
|
+
*, arguments: Any, current_state: str, surface: str
|
|
811
|
+
) -> str:
|
|
812
|
+
"""Resolve the target state for one bulk alert mutation."""
|
|
813
|
+
|
|
814
|
+
target_state = arguments.target_state or current_state
|
|
815
|
+
allowed_states_by_surface = {
|
|
816
|
+
"code-scanning": {"open", "dismissed"},
|
|
817
|
+
"dependabot": {"open", "dismissed"},
|
|
818
|
+
"malware": {"open", "dismissed"},
|
|
819
|
+
"secret-scanning": {"open", "resolved"},
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
allowed_states = allowed_states_by_surface[surface]
|
|
823
|
+
if target_state not in allowed_states:
|
|
824
|
+
raise GitHubSecurityCliError(
|
|
825
|
+
f"Surface '{surface}' does not support target state '{target_state}'."
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
return target_state
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def build_bulk_update_namespace(
|
|
832
|
+
*, alert: dict[str, Any], arguments: Any, surface: str
|
|
833
|
+
) -> SimpleNamespace:
|
|
834
|
+
"""Build an update namespace for one selected bulk alert mutation."""
|
|
835
|
+
|
|
836
|
+
current_state = str(alert.get("state", "unknown"))
|
|
837
|
+
target_state = get_bulk_mutation_target_state(
|
|
838
|
+
arguments=arguments,
|
|
839
|
+
current_state=current_state,
|
|
840
|
+
surface=surface,
|
|
841
|
+
)
|
|
842
|
+
alert_number = alert.get("number")
|
|
843
|
+
if not isinstance(alert_number, int):
|
|
844
|
+
raise GitHubSecurityCliError(
|
|
845
|
+
f"Selected alert is missing an integer alert number for surface '{surface}'."
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
if surface == "code-scanning":
|
|
849
|
+
return SimpleNamespace(
|
|
850
|
+
alert=alert_number,
|
|
851
|
+
assignees=arguments.assignees,
|
|
852
|
+
clear_assignees=arguments.clear_assignees,
|
|
853
|
+
comment=arguments.comment,
|
|
854
|
+
create_request=arguments.create_request,
|
|
855
|
+
dismissed_reason=arguments.dismissed_reason,
|
|
856
|
+
dry_run=arguments.dry_run,
|
|
857
|
+
state=target_state,
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
if surface in {"dependabot", "malware"}:
|
|
861
|
+
return SimpleNamespace(
|
|
862
|
+
alert=alert_number,
|
|
863
|
+
assignees=arguments.assignees,
|
|
864
|
+
clear_assignees=arguments.clear_assignees,
|
|
865
|
+
comment=arguments.comment,
|
|
866
|
+
dismissed_reason=arguments.dismissed_reason,
|
|
867
|
+
dry_run=arguments.dry_run,
|
|
868
|
+
state=target_state,
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
if arguments.assignees is not None and len(arguments.assignees) > 1:
|
|
872
|
+
raise GitHubSecurityCliError(
|
|
873
|
+
"Secret scanning bulk updates accept at most one --assignee value."
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
return SimpleNamespace(
|
|
877
|
+
alert=alert_number,
|
|
878
|
+
assignee=(arguments.assignees[0] if arguments.assignees else None),
|
|
879
|
+
comment=arguments.comment,
|
|
880
|
+
dry_run=arguments.dry_run,
|
|
881
|
+
resolution=arguments.resolution,
|
|
882
|
+
state=target_state,
|
|
883
|
+
unassign=arguments.clear_assignees,
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def summarize_selected_alert(
|
|
888
|
+
alert: dict[str, Any], surface: str
|
|
889
|
+
) -> dict[str, Any]:
|
|
890
|
+
"""Create a compact selected-alert summary for bulk results."""
|
|
891
|
+
|
|
892
|
+
summary = {
|
|
893
|
+
"alert_number": alert.get("number"),
|
|
894
|
+
"html_url": alert.get("html_url"),
|
|
895
|
+
"state": alert.get("state"),
|
|
896
|
+
"surface": surface,
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if surface == "code-scanning":
|
|
900
|
+
rule = alert.get("rule")
|
|
901
|
+
if isinstance(rule, dict):
|
|
902
|
+
summary["rule_id"] = rule.get("id") or rule.get("name")
|
|
903
|
+
summary["severity"] = rule.get(
|
|
904
|
+
"security_severity_level"
|
|
905
|
+
) or rule.get("severity")
|
|
906
|
+
return summary
|
|
907
|
+
|
|
908
|
+
if surface in {"dependabot", "malware"}:
|
|
909
|
+
vulnerability = alert.get("security_vulnerability")
|
|
910
|
+
if isinstance(vulnerability, dict):
|
|
911
|
+
package = vulnerability.get("package")
|
|
912
|
+
if isinstance(package, dict):
|
|
913
|
+
summary["package"] = package.get("name")
|
|
914
|
+
summary["ecosystem"] = package.get("ecosystem")
|
|
915
|
+
summary["severity"] = vulnerability.get("severity")
|
|
916
|
+
dependency = alert.get("dependency")
|
|
917
|
+
if isinstance(dependency, dict):
|
|
918
|
+
summary["manifest_path"] = dependency.get("manifest_path")
|
|
919
|
+
return summary
|
|
920
|
+
|
|
921
|
+
summary["secret_type"] = alert.get("secret_type")
|
|
922
|
+
summary["resolution"] = alert.get("resolution")
|
|
923
|
+
return summary
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def select_bulk_alerts(
|
|
927
|
+
context: RepoContext, arguments: Any
|
|
928
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
929
|
+
"""Resolve the alerts targeted by a bulk-update command."""
|
|
930
|
+
|
|
931
|
+
advisory_cache: dict[str, dict[str, Any] | None] = {}
|
|
932
|
+
lookup_failures: list[dict[str, Any]] = []
|
|
933
|
+
|
|
934
|
+
if arguments.alerts:
|
|
935
|
+
return (
|
|
936
|
+
fetch_selected_bulk_alerts(
|
|
937
|
+
context=context,
|
|
938
|
+
arguments=arguments,
|
|
939
|
+
advisory_cache=advisory_cache,
|
|
940
|
+
),
|
|
941
|
+
lookup_failures,
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
fetched_alerts, lookup_failures = fetch_bulk_alerts_by_surface(
|
|
945
|
+
context=context,
|
|
946
|
+
arguments=arguments,
|
|
947
|
+
advisory_cache=advisory_cache,
|
|
948
|
+
lookup_failures=lookup_failures,
|
|
949
|
+
)
|
|
950
|
+
return fetched_alerts, lookup_failures
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def fetch_selected_bulk_alerts(
|
|
954
|
+
*,
|
|
955
|
+
context: RepoContext,
|
|
956
|
+
arguments: Any,
|
|
957
|
+
advisory_cache: dict[str, dict[str, Any] | None],
|
|
958
|
+
) -> list[dict[str, Any]]:
|
|
959
|
+
selected_alerts: list[dict[str, Any]] = []
|
|
960
|
+
for alert_number in arguments.alerts:
|
|
961
|
+
selected_alerts.append(
|
|
962
|
+
fetch_one_bulk_alert(
|
|
963
|
+
context=context,
|
|
964
|
+
arguments=arguments,
|
|
965
|
+
advisory_cache=advisory_cache,
|
|
966
|
+
alert_number=alert_number,
|
|
967
|
+
)
|
|
968
|
+
)
|
|
969
|
+
return selected_alerts
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def fetch_one_bulk_alert(
|
|
973
|
+
*,
|
|
974
|
+
context: RepoContext,
|
|
975
|
+
arguments: Any,
|
|
976
|
+
advisory_cache: dict[str, dict[str, Any] | None],
|
|
977
|
+
alert_number: int,
|
|
978
|
+
) -> dict[str, Any]:
|
|
979
|
+
if arguments.surface == "code-scanning":
|
|
980
|
+
return fetch_code_scanning_alert(context, alert_number)
|
|
981
|
+
if arguments.surface == "dependabot":
|
|
982
|
+
return fetch_dependabot_alert(context, alert_number)
|
|
983
|
+
if arguments.surface == "malware" and arguments.skip_malware_check:
|
|
984
|
+
return fetch_dependabot_alert(context, alert_number)
|
|
985
|
+
if arguments.surface == "malware":
|
|
986
|
+
return maybe_raise_if_not_malware(context, alert_number, advisory_cache)
|
|
987
|
+
return fetch_secret_scanning_alert(
|
|
988
|
+
context,
|
|
989
|
+
alert_number=alert_number,
|
|
990
|
+
show_secret_values=arguments.show_secret_values,
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def fetch_bulk_alerts_by_surface(
|
|
995
|
+
*,
|
|
996
|
+
context: RepoContext,
|
|
997
|
+
arguments: Any,
|
|
998
|
+
advisory_cache: dict[str, dict[str, Any] | None],
|
|
999
|
+
lookup_failures: list[dict[str, Any]],
|
|
1000
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
1001
|
+
if arguments.surface == "code-scanning":
|
|
1002
|
+
return (
|
|
1003
|
+
fetch_code_scanning_alerts(
|
|
1004
|
+
context, build_bulk_code_scanning_query(arguments)
|
|
1005
|
+
),
|
|
1006
|
+
lookup_failures,
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
if arguments.surface == "dependabot":
|
|
1010
|
+
return (
|
|
1011
|
+
fetch_dependabot_alerts(
|
|
1012
|
+
context, build_bulk_dependabot_query(arguments)
|
|
1013
|
+
),
|
|
1014
|
+
lookup_failures,
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
if arguments.surface == "malware":
|
|
1018
|
+
dependabot_alerts = fetch_dependabot_alerts(
|
|
1019
|
+
context, build_bulk_dependabot_query(arguments)
|
|
1020
|
+
)
|
|
1021
|
+
malware_alerts, lookup_failures = classify_malware_alerts(
|
|
1022
|
+
context,
|
|
1023
|
+
dependabot_alerts,
|
|
1024
|
+
advisory_cache,
|
|
1025
|
+
)
|
|
1026
|
+
return malware_alerts, lookup_failures
|
|
1027
|
+
|
|
1028
|
+
return (
|
|
1029
|
+
fetch_secret_scanning_alerts(
|
|
1030
|
+
context, build_bulk_secret_scanning_query(arguments)
|
|
1031
|
+
),
|
|
1032
|
+
lookup_failures,
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def bulk_update_alerts(context: RepoContext, arguments: Any) -> dict[str, Any]:
|
|
1037
|
+
"""Bulk update alerts across one selected GitHub security surface."""
|
|
1038
|
+
|
|
1039
|
+
selected_alerts, lookup_failures = select_bulk_alerts(context, arguments)
|
|
1040
|
+
if arguments.limit is not None:
|
|
1041
|
+
selected_alerts = selected_alerts[: arguments.limit]
|
|
1042
|
+
|
|
1043
|
+
selected_summaries = [
|
|
1044
|
+
summarize_selected_alert(alert, arguments.surface)
|
|
1045
|
+
for alert in selected_alerts
|
|
1046
|
+
]
|
|
1047
|
+
|
|
1048
|
+
if arguments.dry_run:
|
|
1049
|
+
preview_updates = [
|
|
1050
|
+
build_bulk_update_namespace(
|
|
1051
|
+
alert=alert,
|
|
1052
|
+
arguments=arguments,
|
|
1053
|
+
surface=arguments.surface,
|
|
1054
|
+
).__dict__
|
|
1055
|
+
for alert in selected_alerts
|
|
1056
|
+
]
|
|
1057
|
+
return {
|
|
1058
|
+
"dry_run": True,
|
|
1059
|
+
"lookup_failures": lookup_failures,
|
|
1060
|
+
"selected_alerts": selected_summaries,
|
|
1061
|
+
"selected_count": len(selected_alerts),
|
|
1062
|
+
"surface": arguments.surface,
|
|
1063
|
+
"update_requests": preview_updates,
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
successes: list[dict[str, Any]] = []
|
|
1067
|
+
failures: list[dict[str, Any]] = []
|
|
1068
|
+
|
|
1069
|
+
for alert in selected_alerts:
|
|
1070
|
+
try:
|
|
1071
|
+
update_args = build_bulk_update_namespace(
|
|
1072
|
+
alert=alert,
|
|
1073
|
+
arguments=arguments,
|
|
1074
|
+
surface=arguments.surface,
|
|
1075
|
+
)
|
|
1076
|
+
successes.append(
|
|
1077
|
+
apply_bulk_update(context, arguments.surface, update_args)
|
|
1078
|
+
)
|
|
1079
|
+
except GitHubSecurityCliError as exc:
|
|
1080
|
+
failures.append(
|
|
1081
|
+
{
|
|
1082
|
+
"alert_number": alert.get("number"),
|
|
1083
|
+
"message": str(exc),
|
|
1084
|
+
"type": type(exc).__name__,
|
|
1085
|
+
}
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
return {
|
|
1089
|
+
"failures": failures,
|
|
1090
|
+
"lookup_failures": lookup_failures,
|
|
1091
|
+
"selected_alerts": selected_summaries,
|
|
1092
|
+
"selected_count": len(selected_alerts),
|
|
1093
|
+
"success_count": len(successes),
|
|
1094
|
+
"surface": arguments.surface,
|
|
1095
|
+
"updated_alerts": successes,
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
def apply_bulk_update(
|
|
1100
|
+
context: RepoContext, surface: str, update_args: Any
|
|
1101
|
+
) -> dict[str, Any]:
|
|
1102
|
+
if surface == "code-scanning":
|
|
1103
|
+
return update_code_scanning_alert(context, update_args)
|
|
1104
|
+
if surface in {"dependabot", "malware"}:
|
|
1105
|
+
return update_dependabot_alert(context, update_args)
|
|
1106
|
+
return update_secret_scanning_alert(context, update_args)
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def handle_command(context: RepoContext, arguments: Any) -> Any:
|
|
1110
|
+
"""Dispatch the parsed command."""
|
|
1111
|
+
|
|
1112
|
+
handlers = {
|
|
1113
|
+
"summary": build_summary,
|
|
1114
|
+
"repo-security-overview": command_repo_security_overview,
|
|
1115
|
+
"export-alerts": build_export_alerts,
|
|
1116
|
+
"bulk-update-alerts": bulk_update_alerts,
|
|
1117
|
+
"list-code-scanning": command_list_code_scanning,
|
|
1118
|
+
"show-code-scanning": show_code_scanning_alert,
|
|
1119
|
+
"update-code-scanning": update_code_scanning_alert,
|
|
1120
|
+
"list-dependabot": command_list_dependabot,
|
|
1121
|
+
"show-dependabot": command_show_dependabot,
|
|
1122
|
+
"update-dependabot": update_dependabot_alert,
|
|
1123
|
+
"list-malware": list_malware_alerts,
|
|
1124
|
+
"show-malware": command_show_malware,
|
|
1125
|
+
"update-malware": command_update_malware,
|
|
1126
|
+
"list-secret-scanning": command_list_secret_scanning,
|
|
1127
|
+
"show-secret-scanning": command_show_secret_scanning,
|
|
1128
|
+
"update-secret-scanning": update_secret_scanning_alert,
|
|
1129
|
+
"list-secret-locations": command_list_secret_locations,
|
|
1130
|
+
"secret-scan-history": command_secret_scan_history,
|
|
1131
|
+
"api-call": run_api_call,
|
|
1132
|
+
}
|
|
1133
|
+
try:
|
|
1134
|
+
return handlers[arguments.command](context, arguments)
|
|
1135
|
+
except KeyError as error:
|
|
1136
|
+
raise GitHubSecurityCliError(
|
|
1137
|
+
f"Unsupported command '{arguments.command}'."
|
|
1138
|
+
) from error
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def command_repo_security_overview(
|
|
1142
|
+
context: RepoContext, _arguments: Any
|
|
1143
|
+
) -> dict[str, Any]:
|
|
1144
|
+
return fetch_repository_overview(context)
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def command_list_code_scanning(
|
|
1148
|
+
context: RepoContext, arguments: Any
|
|
1149
|
+
) -> list[dict[str, Any]]:
|
|
1150
|
+
return fetch_code_scanning_alerts(context, build_code_scanning_query(arguments))
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
def command_list_dependabot(
|
|
1154
|
+
context: RepoContext, arguments: Any
|
|
1155
|
+
) -> list[dict[str, Any]]:
|
|
1156
|
+
return fetch_dependabot_alerts(context, build_dependabot_query(arguments))
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
def command_show_dependabot(
|
|
1160
|
+
context: RepoContext, arguments: Any
|
|
1161
|
+
) -> dict[str, Any]:
|
|
1162
|
+
return fetch_dependabot_alert(context, arguments.alert)
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def command_show_malware(context: RepoContext, arguments: Any) -> dict[str, Any]:
|
|
1166
|
+
return maybe_raise_if_not_malware(context, arguments.alert, {})
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def command_update_malware(context: RepoContext, arguments: Any) -> dict[str, Any]:
|
|
1170
|
+
if not arguments.skip_malware_check:
|
|
1171
|
+
maybe_raise_if_not_malware(context, arguments.alert, {})
|
|
1172
|
+
return update_dependabot_alert(context, arguments)
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
def command_list_secret_scanning(
|
|
1176
|
+
context: RepoContext, arguments: Any
|
|
1177
|
+
) -> list[dict[str, Any]]:
|
|
1178
|
+
return fetch_secret_scanning_alerts(
|
|
1179
|
+
context, build_secret_scanning_query(arguments)
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def command_show_secret_scanning(
|
|
1184
|
+
context: RepoContext, arguments: Any
|
|
1185
|
+
) -> dict[str, Any]:
|
|
1186
|
+
return fetch_secret_scanning_alert(
|
|
1187
|
+
context,
|
|
1188
|
+
alert_number=arguments.alert,
|
|
1189
|
+
show_secret_values=arguments.show_secret_values,
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def command_list_secret_locations(
|
|
1194
|
+
context: RepoContext, arguments: Any
|
|
1195
|
+
) -> list[dict[str, Any]]:
|
|
1196
|
+
return fetch_secret_locations(
|
|
1197
|
+
context,
|
|
1198
|
+
alert_number=arguments.alert,
|
|
1199
|
+
page=arguments.page,
|
|
1200
|
+
per_page=arguments.per_page,
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
def command_secret_scan_history(
|
|
1205
|
+
context: RepoContext, _arguments: Any
|
|
1206
|
+
) -> dict[str, Any]:
|
|
1207
|
+
return fetch_secret_scan_history(context)
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
def show_code_scanning_alert(context: RepoContext, arguments: Any) -> dict[str, Any]:
|
|
1211
|
+
alert = fetch_code_scanning_alert(context, arguments.alert)
|
|
1212
|
+
if arguments.include_instances:
|
|
1213
|
+
alert["instances"] = fetch_code_scanning_instances(
|
|
1214
|
+
context,
|
|
1215
|
+
arguments.alert,
|
|
1216
|
+
page=1,
|
|
1217
|
+
per_page=arguments.instances_per_page,
|
|
1218
|
+
)
|
|
1219
|
+
if arguments.include_autofix:
|
|
1220
|
+
alert["autofix"] = fetch_code_scanning_autofix(context, arguments.alert)
|
|
1221
|
+
return alert
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def list_malware_alerts(context: RepoContext, arguments: Any) -> dict[str, Any]:
|
|
1225
|
+
alerts = fetch_dependabot_alerts(context, build_dependabot_query(arguments))
|
|
1226
|
+
malware_alerts, lookup_failures = classify_malware_alerts(context, alerts, {})
|
|
1227
|
+
return {
|
|
1228
|
+
"alerts": malware_alerts,
|
|
1229
|
+
"lookup_failures": lookup_failures,
|
|
1230
|
+
"total": len(malware_alerts),
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
def run_api_call(context: RepoContext, arguments: Any) -> dict[str, Any]:
|
|
1235
|
+
response = api_request(
|
|
1236
|
+
context,
|
|
1237
|
+
endpoint=arguments.endpoint,
|
|
1238
|
+
method=arguments.method,
|
|
1239
|
+
params=parse_name_value_pairs(arguments.query_params),
|
|
1240
|
+
body=None if arguments.body_json is None else json.loads(arguments.body_json),
|
|
1241
|
+
)
|
|
1242
|
+
return {
|
|
1243
|
+
"data": response.data,
|
|
1244
|
+
"status_code": response.status_code,
|
|
1245
|
+
"url": response.url,
|
|
1246
|
+
}
|