github-security-report 0.11.3__tar.gz → 0.12.1__tar.gz

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.
Files changed (72) hide show
  1. {github_security_report-0.11.3 → github_security_report-0.12.1}/PKG-INFO +429 -18
  2. {github_security_report-0.11.3 → github_security_report-0.12.1}/README.md +428 -17
  3. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/_version.py +2 -2
  4. github_security_report-0.12.1/src/github_security_report/authors.py +183 -0
  5. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/categories.py +54 -1
  6. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/cli/__init__.py +2 -0
  7. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/cli/app.py +42 -1
  8. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/cli/modes.py +55 -13
  9. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/cli/outputs.py +87 -15
  10. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/cli/serialise.py +42 -13
  11. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/client/__init__.py +2 -0
  12. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/client/org_reads.py +198 -15
  13. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/client/parsers.py +139 -1
  14. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/client/queries.py +78 -0
  15. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/client/transport.py +208 -94
  16. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/collect/extras.py +46 -3
  17. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/collect/protocols.py +17 -0
  18. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/config/loader.py +19 -0
  19. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/config/models.py +43 -2
  20. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/config/schema.py +15 -6
  21. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/issues.py +60 -7
  22. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/models.py +54 -2
  23. github_security_report-0.12.1/src/github_security_report/ordering.py +440 -0
  24. github_security_report-0.12.1/src/github_security_report/pulls/__init__.py +148 -0
  25. github_security_report-0.12.1/src/github_security_report/pulls/columns.py +53 -0
  26. github_security_report-0.12.1/src/github_security_report/pulls/counting.py +140 -0
  27. github_security_report-0.12.1/src/github_security_report/pulls/presentation.py +133 -0
  28. github_security_report-0.12.1/src/github_security_report/pulls/table.py +218 -0
  29. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/render/html.py +10 -1
  30. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/render/markdown.py +33 -18
  31. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/render/slack.py +21 -3
  32. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/render/terminal.py +50 -7
  33. github_security_report-0.12.1/src/github_security_report/report/__init__.py +103 -0
  34. github_security_report-0.12.1/src/github_security_report/report/aggregate.py +129 -0
  35. github_security_report-0.12.1/src/github_security_report/report/display.py +181 -0
  36. github_security_report-0.12.1/src/github_security_report/report/signals.py +95 -0
  37. github_security_report-0.12.1/src/github_security_report/report/tables.py +148 -0
  38. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/templates/report.html.j2 +20 -1
  39. github_security_report-0.11.3/src/github_security_report/ordering.py +0 -222
  40. github_security_report-0.11.3/src/github_security_report/report.py +0 -419
  41. {github_security_report-0.11.3 → github_security_report-0.12.1}/.gitignore +0 -0
  42. {github_security_report-0.11.3 → github_security_report-0.12.1}/LICENSE +0 -0
  43. {github_security_report-0.11.3 → github_security_report-0.12.1}/LICENSES/Apache-2.0.txt +0 -0
  44. {github_security_report-0.11.3 → github_security_report-0.12.1}/pyproject.toml +0 -0
  45. {github_security_report-0.11.3 → github_security_report-0.12.1}/scripts/README.md +0 -0
  46. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/__init__.py +0 -0
  47. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/classify.py +0 -0
  48. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/cli/__main__.py +0 -0
  49. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/client/endpoints.py +0 -0
  50. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/client/reads.py +0 -0
  51. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/client/writes.py +0 -0
  52. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/collect/__init__.py +0 -0
  53. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/collect/context.py +0 -0
  54. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/collect/org.py +0 -0
  55. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/collect/repo.py +0 -0
  56. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/config/__init__.py +0 -0
  57. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/gating.py +0 -0
  58. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/gitctx.py +0 -0
  59. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/posture/__init__.py +0 -0
  60. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/posture/enablement.py +0 -0
  61. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/posture/facts.py +0 -0
  62. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/posture/releases.py +0 -0
  63. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/py.typed +0 -0
  64. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/remediate.py +0 -0
  65. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/render/__init__.py +0 -0
  66. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/render/slack_limits.py +0 -0
  67. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/rulesets.py +0 -0
  68. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/runner.py +0 -0
  69. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/scope.py +0 -0
  70. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/severity.py +0 -0
  71. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/summary.py +0 -0
  72. {github_security_report-0.11.3 → github_security_report-0.12.1}/src/github_security_report/templates/index.html.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: github-security-report
3
- Version: 0.11.3
3
+ Version: 0.12.1
4
4
  Summary: Security and quality reporting across GitHub organisations
5
5
  Project-URL: Homepage, https://github.com/lfreleng-actions/github-security-report-action
6
6
  Project-URL: Repository, https://github.com/lfreleng-actions/github-security-report-action
@@ -445,30 +445,69 @@ total open issues, then on the oldest issue.
445
445
 
446
446
  Ordering is resolved once, when the report is built, so every surface and
447
447
  `report.json` agree. It applies to the generic tables (GitHub Issues, Releases /
448
- Tagging, Mutable Releases, the Dependabot posture tables). The severity signal
449
- tables keep their own ranking, which encodes domain logic a column sort would
450
- flatten — Scorecard cascades through the worst populated severity rung so a lone
451
- Critical is never buried by a weaker repository with a lower score.
448
+ Tagging, Mutable Releases, the Dependabot posture tables) **and** to the severity
449
+ signal tables (CodeQL, OpenSSF Scorecard, zizmor, AI Slop, Dependabot alerts,
450
+ secret scanning).
451
+
452
+ The signal tables resolve their terms against a fixed vocabulary rather than a
453
+ rendered column list, because their columns vary by surface and by data (Slack
454
+ abbreviates the headers and drops `Total`, and `Info` appears only when some
455
+ repository carries note-level findings):
456
+
457
+ | Signal | Accepted sort names |
458
+ | ------ | ------------------- |
459
+ | OpenSSF Scorecard | `repository`, `score`, `critical`, `high`, `medium`, `low`, `info`, `total` |
460
+ | CodeQL, zizmor, AI Slop, Dependabot alerts | `repository`, `critical`, `high`, `medium`, `low`, `info`, `total` |
461
+ | Secret Scanning | `repository`, `open` |
462
+
463
+ `informational` is accepted for `info`, and `total` for secret scanning's `open`.
464
+
465
+ A bare `score` sorts **ascending**, because the rule is "worst first" and a lower
466
+ Scorecard score is the weaker repository — so `sort: ["score"]` agrees with the
467
+ default ranking instead of contradicting it. Every count sorts descending. A
468
+ repository with no published score sorts last in either direction: unknown health
469
+ is not bad health.
470
+
471
+ Ranking by remediation volume rather than by score is the common case, since the
472
+ score is a health rating and not a count of work:
473
+
474
+ ```json
475
+ {
476
+ "report": {
477
+ "categories": {
478
+ "scorecard": { "sort": ["total", "score"] }
479
+ }
480
+ },
481
+ "organizations": [{ "name": "lfreleng-actions" }]
482
+ }
483
+ ```
484
+
485
+ Omitting `sort` keeps each signal's default ranking, which is not expressible as
486
+ a column list — Scorecard cascades through the worst severity rung any offender
487
+ actually carries, so a lone Critical is never buried by a weaker repository with
488
+ a lower score.
452
489
 
453
490
  ### GitHub Issues
454
491
 
455
492
  The `github_issues` category counts each repository's **open issues**, split by
456
493
  label into columns. It reads from the same batched GraphQL prefetch as the
457
- release and Dependabot data, so it costs no extra API requests:
494
+ release and Dependabot data, so it costs no extra requests per repository; the
495
+ `Ext` column adds one organisation-membership read per run, shared with the
496
+ Pull Requests table.
458
497
 
459
498
  ```text
460
499
  GitHub Issues
461
- ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━┳━━━━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━┓
462
- ┃ Repository ┃ Bug ┃ Feature ┃ Docs ┃ Other ┃ Untriaged ┃ Total ┃ Oldest ┃
463
- ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━╇━━━━━━╇━━━━━━━╇━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━┩
464
- │ .github │ 1 │ 0 │ 0 │ 0 │ 10 │ 11 │ 16 days │
465
- │ tag-validate-action │ 0 │ 0 │ 0 │ 0 │ 8 │ 8 │ 25 days │
466
- │ security-workflows │ 0 │ 5 │ 1 │ 0 │ 0 │ 6 │ today │
467
- │ github-security-report-action │ 0 │ 0 │ 0 │ 3 │ 1 │ 4 │ 52 days │
468
- │ dependamerge │ 0 │ 1 │ 0 │ 1 │ 1 │ 3 │ 52 days │
469
- ├───────────────────────────────┼─────┼─────────┼──────┼───────┼───────────┼───────┼─────────┤
470
- │ Total │ 1 │ 6 │ 1 │ 4 │ 20 │ 32 │ │
471
- └───────────────────────────────┴─────┴─────────┴──────┴───────┴───────────┴───────┴─────────┘
500
+ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━┳━━━━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┳━━━━━┳━━━━━━━━━┓
501
+ ┃ Repository ┃ Bug ┃ Feature ┃ Docs ┃ Other ┃ Untriaged ┃ Total ┃ Ext ┃ Oldest ┃
502
+ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━╇━━━━━━╇━━━━━━━╇━━━━━━━━━━━╇━━━━━━━╇━━━━━╇━━━━━━━━━┩
503
+ │ .github │ 1 │ 0 │ 0 │ 0 │ 10 │ 11 │ 0 16 days │
504
+ │ tag-validate-action │ 0 │ 0 │ 0 │ 0 │ 8 │ 8 │ 0 25 days │
505
+ │ security-workflows │ 0 │ 5 │ 1 │ 0 │ 0 │ 6 │ 0 │ today │
506
+ │ github-security-report-action │ 0 │ 0 │ 0 │ 3 │ 1 │ 4 │ 0 52 days │
507
+ │ dependamerge │ 0 │ 1 │ 0 │ 1 │ 1 │ 3 │ 0 52 days │
508
+ ├───────────────────────────────┼─────┼─────────┼──────┼───────┼───────────┼───────┼─────┼─────────┤
509
+ │ Total │ 1 │ 6 │ 1 │ 4 │ 20 │ 32 │ 0 │
510
+ └───────────────────────────────┴─────┴─────────┴──────┴───────┴───────────┴───────┴─────┴─────────┘
472
511
  … and 11 more
473
512
  ❌ 16 With open issues
474
513
  ✅ 87 No open issues
@@ -485,7 +524,11 @@ Two columns are always present and are not configurable:
485
524
  - **Untriaged** — the issue has no labels at all. This is the column to watch:
486
525
  an unlabelled issue is one nobody has categorised.
487
526
 
488
- Both names are reserved, as are `Repository`, `Total` and `Oldest`: configuring a
527
+ A third, **Ext**, counts the issues raised from **outside the organisation**
528
+ see [Inside or outside the organisation](#inside-or-outside-the-organisation).
529
+
530
+ All three names are reserved, as are `Repository`, `Total` and `Oldest`:
531
+ configuring a
489
532
  column with one of those names is rejected, because it would either share a
490
533
  counter with the implicit column — stopping the class columns summing to `Total`
491
534
  — or duplicate a header, which would also make `sort: ["repository"]` resolve to
@@ -541,6 +584,352 @@ with HTTP 200 and this one field null, so affected repositories are reported as
541
584
  `❓ Unknown` rather than counted as having no open issues — an unreadable backlog
542
585
  is never presented as a clean one.
543
586
 
587
+ ### Pull Requests
588
+
589
+ The `pull_requests` category counts each repository's **open pull requests**,
590
+ split by who raised them and by what is holding them up. The per-repository data
591
+ rides the same batched GraphQL prefetch as the issues data, so it costs **no
592
+ extra requests per repository** — measured against a five-repository batch,
593
+ adding the connection, the head commit's check rollup and the assignee list
594
+ moved the query cost from 1 point to 4, against an hourly budget of 5,000.
595
+
596
+ Identifying contributors does add **two requests per organisation**, made once
597
+ per run and reused by every repository and both author-aware tables:
598
+ organisation membership (one more per 100 members) and the authenticated
599
+ account. See [Inside or outside the
600
+ organisation](#inside-or-outside-the-organisation):
601
+
602
+ ```text
603
+ Pull Requests
604
+ ┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━┳━━━━━━┳━━━━━━━━━━┳━━━━━━┳━━━━━━━┳━━━━━━━┓
605
+ ┃ Repository ┃ Human ┃ Ext ┃ Auto ┃ Conflict ┃ Fail ┃ Draft ┃ Total ┃
606
+ ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━╇━━━━━━╇━━━━━━━━━━╇━━━━━━╇━━━━━━━╇━━━━━━━┩
607
+ │ lftools-uv │ 8 │ 0 │ 0 │ 0 │ 0 │ 0 │ 8 │
608
+ │ dependamerge │ 3 │ 0 │ 0 │ 0 │ 0 │ 0 │ 3 │
609
+ │ gha-workflow-linter │ 2 │ 0 │ 0 │ 0 │ 1 │ 1 │ 2 │
610
+ │ harden-runner-block- │ 1 │ 0 │ 0 │ 1 │ 0 │ 1 │ 1 │
611
+ │ action │ │ │ │ │ │ │ │
612
+ ├─────────────────────┼───────┼─────┼──────┼──────────┼──────┼───────┼───────┤
613
+ │ Total │ 14 │ 0 │ 0 │ 1 │ 1 │ 2 │ 14 │
614
+ └─────────────────────┴───────┴─────┴──────┴──────────┴──────┴───────┴───────┘
615
+ … and 9 more
616
+ ❌ 13 With open pull requests
617
+ ✅ 104 No open pull requests
618
+ ```
619
+
620
+ The columns form **two independent groupings**:
621
+
622
+ | Column | Meaning |
623
+ | ------ | ------- |
624
+ | `Human` / `Auto` | Partition the total by author. `Auto` is recognised automation; `Human` is everyone else. |
625
+ | `Ext` | Human pull requests raised from outside the organisation — a **subset of Human**, which is why it sits beside it. |
626
+ | `Conflict` | Blocked on a merge conflict. |
627
+ | `Fail` | Latest checks did not pass. The rollup includes optional checks, so this is not by itself proof that the merge is blocked. |
628
+ | `Draft` | Marked as a draft. |
629
+
630
+ `Conflict`, `Fail` and `Draft` **overlap** each other and the author split, so
631
+ they do not sum to `Total` and are not meant to: one pull request that is
632
+ conflicting, failing *and* a draft is counted once in each of those three
633
+ columns, and once under `Human` or `Auto`. Only `Human` + `Auto` reconciles with
634
+ the collected total. They are ordered worst-first — a conflict needs a human to
635
+ rebase, a failing check may only need a re-run, and a draft is not blocked at
636
+ all.
637
+
638
+ #### Automation backlog thresholds
639
+
640
+ On the terminal the counts are coloured so the table reads at a glance: `Human`
641
+ and `Ext` **green**, `Conflict` and `Fail` **red**, and `Auto` coloured against
642
+ two configured thresholds.
643
+
644
+ Dependabot stops raising pull requests once a repository reaches its
645
+ `open-pull-requests-limit`, so the repository quietly stops receiving dependency
646
+ updates — an outage in waiting rather than an untidy queue. Two thresholds let
647
+ you watch the automation backlog against that risk:
648
+
649
+ ```json
650
+ {
651
+ "report": {
652
+ "dependabot_warn_threshold": 2,
653
+ "dependabot_error_threshold": 5
654
+ },
655
+ "organizations": [{ "name": "lfreleng-actions" }]
656
+ }
657
+ ```
658
+
659
+ - **Yellow** above `dependabot_warn_threshold` (default `2`) — a backlog worth
660
+ keeping an eye on.
661
+ - **Red** at or above `dependabot_error_threshold` (default `5`) — a backlog
662
+ worth investigating now.
663
+
664
+ > **These are configured investigation levels, not measurements.** `Auto` is an
665
+ > aggregate across every automation author, while GitHub applies its
666
+ > `open-pull-requests-limit` **per package ecosystem**. So neither level
667
+ > establishes that Dependabot has stopped: a repository can sit above the red
668
+ > threshold on Renovate pull requests alone, or be stalled in one ecosystem
669
+ > while sitting below it. What the colour tells you is that the backlog has
670
+ > reached a size you said you wanted to hear about, which is worth a look
671
+ > either way.
672
+
673
+ The defaults track **GitHub's own** `open-pull-requests-limit`, which defaults
674
+ to `5`. If your organisation raises that limit, raise these to match — they
675
+ describe your policy, and the defaults are only the most useful guess in the
676
+ absence of one:
677
+
678
+ ```json
679
+ {
680
+ "report": {
681
+ "dependabot_warn_threshold": 12,
682
+ "dependabot_error_threshold": 15
683
+ },
684
+ "organizations": [{ "name": "lfreleng-actions" }]
685
+ }
686
+ ```
687
+
688
+ Either may be set to `0` to switch that level off, matching the `0 = no limit`
689
+ idiom the row limits use. `dependabot_error_threshold` must be **strictly
690
+ greater** than `dependabot_warn_threshold` (or `0`): because the error level is
691
+ checked first and inclusively, an equal or lower error threshold would make the
692
+ warning colour unreachable, so it is rejected at load rather than silently
693
+ ignored. Both are settable per organisation.
694
+
695
+ > **A note on the name.** These are named for Dependabot because its
696
+ > `open-pull-requests-limit` is the constraint the defaults track.
697
+
698
+ Only **non-zero** counts are coloured. A table of red zeros would train the eye
699
+ to ignore the colour, which costs exactly the signal it exists to carry, so a
700
+ clean row stays plain and the rows with something wrong stand out. The totals
701
+ row is never coloured: it sums columns that disagree about what good looks like.
702
+ Colour is a terminal affordance — Markdown and Slack have none, and the HTML
703
+ pages style their tables from the stylesheet.
704
+
705
+ #### Assignment breakdown
706
+
707
+ Beneath the totals, the same pull requests are split again by **who is expected
708
+ to move them**:
709
+
710
+ ```text
711
+ ├─────────────────────┼───────┼─────┼──────┼──────────┼──────┼───────┼───────┤
712
+ │ Total │ 17 │ 0 │ 0 │ 1 │ 1 │ 2 │ 17 │
713
+ ├─────────────────────┼───────┼─────┼──────┼──────────┼──────┼───────┼───────┤
714
+ │ Unassigned │ │ │ │ │ │ │ 1 │
715
+ │ Others │ │ │ │ │ │ │ 14 │
716
+ │ Mine │ │ │ │ │ │ │ 2 │
717
+ └─────────────────────┴───────┴─────┴──────┴──────────┴──────┴───────┴───────┘
718
+ ```
719
+
720
+ The values sit under **`Total`**, because that is what they are a breakdown of:
721
+ they partition every collected pull request, automation included, so aligning
722
+ them under `Human` would file an unassigned bot pull request as a human one.
723
+
724
+ This is a **partition**, not more columns: every collected pull request falls in
725
+ exactly one bucket. It is a breakdown *of* the rows rather than *within* them,
726
+ which is why it cannot be a column without counting the same pull requests
727
+ twice.
728
+
729
+ - **Unassigned** — nobody has picked it up.
730
+ - **Mine** — assigned to the account the report ran as.
731
+ - **Others** — assigned to somebody else.
732
+
733
+ A pull request assigned to several people, one of them you, counts as **Mine**:
734
+ it is in your queue regardless of who else is on it. Like the totals row, the
735
+ breakdown sums the **displayed** rows, so a row limit does not put it out of
736
+ step with the table above it.
737
+
738
+ On rows **not** marked `+`, the buckets reconcile against `Total`. On a marked
739
+ row they cannot: the buckets cover only the collected window while `Total`
740
+ remains the exact backlog, which is what the marker is there to warn you about.
741
+
742
+ ##### Only `Unassigned` is universal
743
+
744
+ `Unassigned` is a fact about the pull request. `Mine` and `Others` are read
745
+ against the account the report authenticated as, and are drawn **only** where
746
+ that reading holds — which needs two things to be true at once:
747
+
748
+ 1. **The run authenticated as a person**, i.e. `viewer { login }` resolved to a
749
+ human account. A bot or App token has no queue, so `Mine` would be empty by
750
+ construction and `Others` would collapse into "assigned to somebody" — a
751
+ split drawn against a person who is not there.
752
+ 2. **The surface is read by that account**, i.e. the terminal. A Pages site,
753
+ Slack digest, `report.md` or `report.json` is read by everybody *except* the
754
+ token owner, for whom `Mine` names a stranger's queue as their own.
755
+
756
+ Where either fails, the breakdown is a single `Unassigned` row. Nothing is lost:
757
+ the assigned count is the totals row minus that one figure. So the daily
758
+ scheduled run publishes this to Pages, whatever token it holds:
759
+
760
+ ```text
761
+ ├─────────────────────┼───────┼─────┼──────┼──────────┼──────┼───────┼───────┤
762
+ │ Total │ 17 │ 0 │ 0 │ 1 │ 1 │ 2 │ 17 │
763
+ ├─────────────────────┼───────┼─────┼──────┼──────────┼──────┼───────┼───────┤
764
+ │ Unassigned │ │ │ │ │ │ │ 1 │
765
+ └─────────────────────┴───────┴─────┴──────┴──────────┴──────┴───────┴───────┘
766
+ ```
767
+
768
+ This is not a configurable toggle. A published `Mine` is not a preference but a
769
+ false statement, so there is nothing to opt into.
770
+
771
+ ### Assigned to Me
772
+
773
+ The `pull_requests_assigned` category repeats the Pull Requests table narrowed
774
+ to pull requests assigned to the account the report ran as — the same columns
775
+ over the same data, so the two read alike, with only the population changed. A
776
+ repository with open pull requests but none of yours counts as clean rather than
777
+ appearing as a row of zeros, keeping the table to your actual inbox.
778
+
779
+ > **"Mine" follows the token, not a configured name.** It is whoever
780
+ > `viewer { login }` resolves to, so the same report run with a different
781
+ > token legitimately answers a different question. A scheduled run under a bot
782
+ > or GitHub App token has no personal queue at all: the category is not
783
+ > collected, and the section is **absent** rather than reporting every
784
+ > repository clean — a reassuring "nothing assigned to you" about an inbox that
785
+ > does not exist is worse than saying nothing.
786
+
787
+ **It is a terminal-only table by default.** A personal review queue keyed to
788
+ whichever account ran the report has no business in a published Pages site or a
789
+ shared Slack digest, so `pull_requests_assigned` ships with `cli` on and
790
+ `markdown`, `html` and `slack` off. No configuration is needed to keep it out of
791
+ shared output, and tuning another of its settings (a `top_n`, say) will not
792
+ silently publish it, since the per-category blocks merge key by key. Opt in
793
+ deliberately if you want it published:
794
+
795
+ ```json
796
+ {
797
+ "report": {
798
+ "categories": {
799
+ "pull_requests_assigned": { "outputs": { "html": true } }
800
+ }
801
+ },
802
+ "organizations": [{ "name": "lfreleng-actions" }]
803
+ }
804
+ ```
805
+
806
+ If you publish it from a workflow, make sure the Action's `hide` input does not
807
+ name the category as well: `--hide` outranks this toggle and cannot be
808
+ countermanded by it. The input is empty by default, so this only matters if you
809
+ have set it.
810
+
811
+ ### Hiding a category for one run
812
+
813
+ `--hide <category>` (repeatable) suppresses a category on **every** output for
814
+ that invocation, and **outranks the configuration**:
815
+
816
+ ```bash
817
+ github-security-report report --hide pull_requests_assigned
818
+ ```
819
+
820
+ The Action exposes the same control through a `hide` input:
821
+
822
+ ```yaml
823
+ - uses: lfreleng-actions/github-security-report-action@v1
824
+ with:
825
+ hide: "pull_requests_assigned" # space- or comma-separated
826
+ ```
827
+
828
+ It is **empty by default**, and does not need setting to keep the personal
829
+ queue out of shared artifacts — `pull_requests_assigned` already defaults to the
830
+ terminal only, so Pages, Markdown and Slack never render it, and `report.json`
831
+ omits it for the same reason (see below). Leaving the input empty also keeps the
832
+ Action compatible with whichever published version the runtime pin installs: a
833
+ non-empty default would be passed to releases predating the flag, which reject
834
+ it outright.
835
+
836
+ It is deliberately **one-way**: naming a category can only suppress it, never
837
+ re-enable one the configuration disabled. That means a CI invocation can keep
838
+ something off a published surface without knowing, or contradicting, what the
839
+ shared configuration asked for — and cannot accidentally publish something an
840
+ operator switched off. An unrecognised category name is rejected with exit `2`
841
+ rather than ignored, since a silently-ignored typo would publish the very thing
842
+ the flag was meant to hide.
843
+
844
+ `report.json` **does** honour `--hide`, and it also omits any category that no
845
+ published surface carries. It is otherwise the complete dataset: the per-surface
846
+ `outputs` toggles do not filter it, so a category hidden from just the terminal,
847
+ or just Slack, is still present in full.
848
+
849
+ The two exceptions exist because the file is written into the **published**
850
+ Pages directory alongside the HTML. Ignoring `--hide` there would publish
851
+ exactly what you asked to keep out of shared output; and serialising a
852
+ terminal-only category would publish it through the back door while every
853
+ rendered surface correctly omitted it. Opting such a category into any
854
+ published surface puts it back in the JSON.
855
+
856
+ `Auto` recognises the same automation accounts as the
857
+ [`dependamerge`](https://github.com/lfreleng-actions/dependamerge) tool:
858
+ Dependabot, Renovate, pre-commit.ci, `github-actions`, Copilot and
859
+ Allcontributors, by their bare or `[bot]`-suffixed login, plus any actor GitHub
860
+ reports as an App and any unrecognised login carrying the `[bot]` marker — so a
861
+ future bot is classified as automation rather than mistaken for an outside
862
+ contributor.
863
+
864
+ `Fail` and `Conflict` count only **established** states. GitHub computes
865
+ mergeability lazily and answers `UNKNOWN` until it settles, and reports no check
866
+ rollup at all when no checks have run; neither absence is evidence that a pull
867
+ request is ready, so neither is counted either way.
868
+
869
+ Repositories with no open pull requests are counted in the footer rather than
870
+ listed. Rows rank by total open pull requests, then by those failing or
871
+ conflicting (`Fail` + `Conflict`), so two repositories with equal backlogs
872
+ surface the more
873
+ stuck one first.
874
+
875
+ > **Accuracy note.** As with the issues table, `Total` is exact at any size,
876
+ > while the breakdown columns are computed from a bounded, oldest-first window
877
+ > of 25 open pull requests per repository. A repository whose backlog exceeds
878
+ > that window shows a trailing `+` on its `Total` cell, marking the breakdown as
879
+ > partial; the total itself stays exact.
880
+
881
+ **Permissions.** A fine-grained PAT needs **Pull requests: read**; a classic
882
+ PAT's `repo` scope already covers it. Without it the connection comes back null
883
+ and affected repositories are reported as `❓ Unknown` rather than as having no
884
+ open pull requests.
885
+
886
+ ### Inside or outside the organisation
887
+
888
+ The `Ext` column on both the Issues and Pull Requests tables counts
889
+ contributions from **outside the organisation**. Two pieces of evidence decide
890
+ it, in this order:
891
+
892
+ 1. **The organisation's membership**, collected once per organisation in a
893
+ single GraphQL query (one page per 100 members) and reused for every
894
+ repository and every table.
895
+ 2. **GitHub's per-item `authorAssociation`**, consulted when the author is not a
896
+ known member. This is what recognises a repository-level *collaborator* who
897
+ holds no organisation membership.
898
+
899
+ The membership query is not redundant, and this ordering is deliberate.
900
+ `authorAssociation` is computed **relative to the requesting token**: where an
901
+ organisation's members keep their membership private — GitHub's default — the
902
+ same issue reports `MEMBER` to a token with organisation visibility and
903
+ `CONTRIBUTOR` to one without. Classifying on that field alone would make the
904
+ counts depend on which token produced the report, and a token lacking
905
+ `read:org` would file an entire organisation as outsiders.
906
+
907
+ **When membership cannot be read at all**, the fallback is not enough on its
908
+ own: an association naming an *insider* is still trusted (only a token that can
909
+ see the relationship reports it), but one naming an outsider proves nothing,
910
+ because that is exactly how a private member appears to an under-privileged
911
+ token. Those authors are reported as indeterminate rather than external, so
912
+ `Ext` **undercounts and should be read as a lower bound** — the table says so in
913
+ its description whenever this happens, and the run logs a warning. Membership is
914
+ never used partially: a failed page or an organisation beyond the pagination
915
+ guard is treated as unreadable rather than as a member list with people missing
916
+ from it, since every absent member would otherwise read as an outsider.
917
+
918
+ **Automation is never counted as external.** Bots are outsiders by association —
919
+ `dependabot[bot]` genuinely reports `CONTRIBUTOR` or `NONE` — so counting on
920
+ association alone would file every dependency-update pull request as an external
921
+ contribution and bury the genuine outside contributors the column exists to
922
+ surface. Automation is reported under `Auto` instead.
923
+
924
+ An author who cannot be classified at all — a deleted account, or an
925
+ association value GitHub has newly introduced — is **not** counted as external.
926
+ The column understates rather than inventing an outsider.
927
+
928
+ **Permissions.** Reading organisation membership needs `read:org` (classic) or
929
+ **Members: read** (fine-grained). Without it the tool logs a warning and
930
+ reports externally-raised counts as a lower bound rather than guessing from
931
+ `authorAssociation` alone.
932
+
544
933
  ### Organisation feature gating
545
934
 
546
935
  The workflow-driven signals (OpenSSF Scorecard, zizmor, aislop) need
@@ -642,6 +1031,7 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
642
1031
  | `top_n_slack` | No | — | Offenders per signal in the Slack digest (`0` = no limit; overrides `top_n`) |
643
1032
  | `fail_threshold` | No | `none` | `none`/`low`/`medium`/`high`/`critical`/`any` (repo mode) |
644
1033
  | `force_notify` | No | `false` | Post to Slack regardless of `report_day` |
1034
+ | `hide` | No | `""` | Category keys to suppress on every output (space- or comma-separated). Overrides config, and is one-way: it cannot re-enable a disabled category |
645
1035
  | `tool_version` | No | `""` | Published PyPI version to install. Empty (the default) uses the Dependabot-managed pin in `.github/runtime-pin/requirements.txt`; set a specific version to override. Ignored on pull requests or when `use_local_source` is `true` (both run from source) |
646
1036
  | `use_local_source` | No | `false` | Run from the checked-out source instead of PyPI (for testing unreleased code from any event) |
647
1037
 
@@ -678,6 +1068,27 @@ your enterprise endpoints (Actions sets these automatically on GHES
678
1068
  runners). `SCORECARD_API_URL` overrides the external OpenSSF Scorecard
679
1069
  API in the same way.
680
1070
 
1071
+ ### Exit codes
1072
+
1073
+ | Code | Meaning |
1074
+ | ---- | ------- |
1075
+ | `0` | The report ran. |
1076
+ | `1` | Repo mode only: findings met or exceeded `--fail-threshold`. |
1077
+ | `2` | Usage or configuration error (bad flag, unreadable config). |
1078
+ | `3` | The GitHub API was unreachable after the retry budget. |
1079
+ | `4` | GitHub rejected the credentials (HTTP 401). |
1080
+
1081
+ Codes `3` and `4` are **aborts, not reports**: nothing is written and no Pages
1082
+ artifact is produced. That is deliberate. A token that has expired, been revoked
1083
+ or been rotated makes every read fail, and a run that degraded instead would
1084
+ render a complete, confidently clean report — `0 repositories analysed`, every
1085
+ section `No data` or `All Clean` — and a scheduled job would then publish it
1086
+ over the last good one. Reporting false data is worse than reporting none, so
1087
+ the run stops at the first rejected request.
1088
+
1089
+ The two are separate codes because the remedy differs: `4` means rotate or fix
1090
+ the token, `3` means retry later.
1091
+
681
1092
  ## Remediation
682
1093
 
683
1094
  The `remediate` subcommand is the in-tool counterpart to the report: it runs the