github-security-report 0.14.2__tar.gz → 0.15.0__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.
- {github_security_report-0.14.2 → github_security_report-0.15.0}/PKG-INFO +78 -34
- {github_security_report-0.14.2 → github_security_report-0.15.0}/README.md +77 -33
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/_version.py +2 -2
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/categories.py +5 -3
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/client/parsers.py +2 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/client/queries.py +16 -0
- github_security_report-0.15.0/src/github_security_report/client/reviews.py +76 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/models.py +22 -71
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/ordering.py +2 -2
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/pulls/__init__.py +4 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/pulls/columns.py +10 -3
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/pulls/counting.py +24 -4
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/pulls/presentation.py +21 -1
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/pulls/table.py +4 -0
- github_security_report-0.15.0/src/github_security_report/ranking.py +86 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/report/__init__.py +1 -1
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/report/aggregate.py +1 -1
- {github_security_report-0.14.2 → github_security_report-0.15.0}/.gitignore +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/LICENSE +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/LICENSES/Apache-2.0.txt +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/pyproject.toml +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/scripts/README.md +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/__init__.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/authors.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/classify.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/cli/__init__.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/cli/__main__.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/cli/app.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/cli/boundary.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/cli/modes.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/cli/options.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/cli/outputs.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/cli/publish.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/cli/serialise.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/client/__init__.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/client/alerts.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/client/batch_errors.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/client/copilot.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/client/endpoints.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/client/errors.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/client/org_reads.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/client/reads.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/client/transport.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/client/writes.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/collect/__init__.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/collect/context.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/collect/extras.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/collect/org.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/collect/protocols.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/collect/repo.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/config/__init__.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/config/loader.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/config/models.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/config/order.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/config/schema.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/gating.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/gitctx.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/issues.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/layout.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/posture/__init__.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/posture/enablement.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/posture/facts.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/posture/releases.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/py.typed +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/remediate.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/render/__init__.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/render/html.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/render/markdown.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/render/slack.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/render/slack_limits.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/render/terminal.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/report/display.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/report/signals.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/report/tables.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/rulesets.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/runner.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/scope.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/secret_patterns.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/severity.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/summary.py +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/templates/index.html.j2 +0 -0
- {github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/templates/report.html.j2 +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: github-security-report
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0
|
|
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
|
|
@@ -736,17 +736,17 @@ organisation](#inside-or-outside-the-organisation):
|
|
|
736
736
|
|
|
737
737
|
```text
|
|
738
738
|
Pull Requests
|
|
739
|
-
|
|
740
|
-
┃ Repository ┃ Human ┃ Ext ┃ Auto ┃ Conflict ┃ Fail ┃ Copilot ┃ Draft ┃ Total ┃
|
|
741
|
-
|
|
742
|
-
│ lftools-uv │ 8 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 8 │
|
|
743
|
-
│ dependamerge │ 3 │ 0 │ 0 │ 0 │ 0 │ 2 │ 0 │ 3 │
|
|
744
|
-
│ gha-workflow-linter │ 2 │ 0 │ 0 │ 0 │ 1 │ 0 │ 1 │ 2 │
|
|
745
|
-
│ harden-runner-block- │ 1 │ 0 │ 0 │ 1 │ 0 │ 0 │ 1 │ 1 │
|
|
746
|
-
│ action │ │ │ │ │ │ │ │ │
|
|
747
|
-
|
|
748
|
-
│ Total │ 14 │ 0 │ 0 │ 1 │ 1 │ 2 │ 2 │ 14 │
|
|
749
|
-
|
|
739
|
+
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━┳━━━━━━┳━━━━━━━━━━┳━━━━━━┳━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━┓
|
|
740
|
+
┃ Repository ┃ Human ┃ Ext ┃ Auto ┃ Conflict ┃ Fail ┃ Review ┃ Copilot ┃ Draft ┃ Total ┃
|
|
741
|
+
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━╇━━━━━━╇━━━━━━━━━━╇━━━━━━╇━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━┩
|
|
742
|
+
│ lftools-uv │ 8 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 8 │
|
|
743
|
+
│ dependamerge │ 3 │ 0 │ 0 │ 0 │ 0 │ 1 │ 2 │ 0 │ 3 │
|
|
744
|
+
│ gha-workflow-linter │ 2 │ 0 │ 0 │ 0 │ 1 │ 0 │ 0 │ 1 │ 2 │
|
|
745
|
+
│ harden-runner-block- │ 1 │ 0 │ 0 │ 1 │ 0 │ 0 │ 0 │ 1 │ 1 │
|
|
746
|
+
│ action │ │ │ │ │ │ │ │ │ │
|
|
747
|
+
├──────────────────────┼───────┼─────┼──────┼──────────┼──────┼────────┼─────────┼───────┼───────┤
|
|
748
|
+
│ Total │ 14 │ 0 │ 0 │ 1 │ 1 │ 1 │ 2 │ 2 │ 14 │
|
|
749
|
+
└──────────────────────┴───────┴─────┴──────┴──────────┴──────┴────────┴─────────┴───────┴───────┘
|
|
750
750
|
… and 9 more
|
|
751
751
|
❌ 13 With open pull requests
|
|
752
752
|
✅ 104 No open pull requests
|
|
@@ -760,34 +760,73 @@ The columns form **two independent groupings**:
|
|
|
760
760
|
| `Ext` | Human pull requests raised from outside the organisation — a **subset of Human**, which is why it sits beside it. |
|
|
761
761
|
| `Conflict` | Blocked on a merge conflict. |
|
|
762
762
|
| `Fail` | Latest checks did not pass. The rollup includes optional checks, so this is not by itself proof that the merge is blocked. |
|
|
763
|
+
| `Review` | A reviewer has **requested changes** — a person waiting on the author. |
|
|
763
764
|
| `Copilot` | Carries at least one **unresolved review thread** opened by GitHub's automated code reviewer. |
|
|
764
765
|
| `Draft` | Marked as a draft. |
|
|
765
766
|
|
|
766
|
-
`Conflict`, `Fail`, `Copilot` and `Draft` **overlap** each other and
|
|
767
|
-
split, so they do not sum to `Total` and are not meant to: one pull
|
|
768
|
-
is conflicting, failing *and* a draft is counted once in each of
|
|
769
|
-
columns, and once under `Human` or `Auto`. Only `Human` + `Auto`
|
|
770
|
-
the collected total. They are ordered worst-first — a conflict
|
|
771
|
-
rebase, a failing check may only need a re-run,
|
|
772
|
-
|
|
773
|
-
all.
|
|
767
|
+
`Conflict`, `Fail`, `Review`, `Copilot` and `Draft` **overlap** each other and
|
|
768
|
+
the author split, so they do not sum to `Total` and are not meant to: one pull
|
|
769
|
+
request that is conflicting, failing *and* a draft is counted once in each of
|
|
770
|
+
those three columns, and once under `Human` or `Auto`. Only `Human` + `Auto`
|
|
771
|
+
reconciles with the collected total. They are ordered worst-first — a conflict
|
|
772
|
+
needs a human to rebase, a failing check may only need a re-run, requested
|
|
773
|
+
changes need the author to act, unresolved automated feedback needs somebody but
|
|
774
|
+
does not hold the merge button down, and a draft is not blocked at all.
|
|
775
|
+
|
|
776
|
+
`Review` and `Copilot` sit together inside the blocker group, and deliberately
|
|
777
|
+
not beside `Human`: that column names who **raised** a pull request, whereas
|
|
778
|
+
these two name who is **waiting on** it. The grouping is what keeps those two
|
|
779
|
+
readings of "human" apart.
|
|
780
|
+
|
|
781
|
+
#### Requested changes
|
|
782
|
+
|
|
783
|
+
`Review` counts pull requests where **a person** has asked for changes and not
|
|
784
|
+
withdrawn it.
|
|
785
|
+
|
|
786
|
+
Two fields answer that, each covering the other's blind spot. `reviewDecision` is
|
|
787
|
+
GitHub's own verdict, computed over every review and accounting for dismissals,
|
|
788
|
+
so it is **exact at any review count** — but it names nobody, and a GitHub App
|
|
789
|
+
holding pull-request write permission can request changes exactly as a person
|
|
790
|
+
can. `latestOpinionatedReviews` names the reviewers, but is a bounded window of
|
|
791
|
+
five (one review per reviewer, so five reviewers rather than five reviews).
|
|
792
|
+
|
|
793
|
+
So the decision **gates** and the window **attributes**. Anything other than
|
|
794
|
+
`CHANGES_REQUESTED` is a definite zero whatever the window holds, and only once
|
|
795
|
+
GitHub says changes are outstanding does the reviewer's identity matter — which
|
|
796
|
+
keeps the cheap exact answer for almost every pull request and pays the window's
|
|
797
|
+
uncertainty on the few where it changes the reading. Automated reviewers are
|
|
798
|
+
recognised by the same rule as the `Auto` column.
|
|
799
|
+
|
|
800
|
+
Only `CHANGES_REQUESTED` gates. **`REVIEW_REQUIRED` is deliberately excluded**:
|
|
801
|
+
it reports that a branch rule demands a review, not that anybody objected, so on
|
|
802
|
+
an organisation that requires review by default it would mark nearly every human
|
|
803
|
+
pull request and distinguish none of them. The rule is written as an allow-list
|
|
804
|
+
of the one blocking state rather than as "anything that is not `APPROVED`", so a
|
|
805
|
+
state GitHub adds later arrives uncounted rather than pre-counted as a blocker.
|
|
806
|
+
|
|
807
|
+
Where a request for changes cannot be attributed — the reviews were unreadable,
|
|
808
|
+
the author is gone, or the window held only automated requests without covering
|
|
809
|
+
every reviewer — the pull request is left **uncounted rather than credited to a
|
|
810
|
+
person**, and the table says so in its description.
|
|
774
811
|
|
|
775
812
|
#### Unresolved Copilot feedback
|
|
776
813
|
|
|
777
|
-
`Copilot` counts the pull requests **waiting on
|
|
778
|
-
review thread counts when it was opened by GitHub's automated code
|
|
779
|
-
`copilot-pull-request-reviewer` bot) and nobody has resolved it.
|
|
780
|
-
identified from each thread's **opening** comment, since later
|
|
781
|
-
usually the human answering the review.
|
|
814
|
+
`Copilot` counts the pull requests **waiting on somebody to answer an automated
|
|
815
|
+
review**. A review thread counts when it was opened by GitHub's automated code
|
|
816
|
+
reviewer (the `copilot-pull-request-reviewer` bot) and nobody has resolved it.
|
|
817
|
+
The reviewer is identified from each thread's **opening** comment, since later
|
|
818
|
+
replies are usually the human answering the review.
|
|
782
819
|
|
|
783
820
|
An **outdated** thread still counts: GitHub marks a thread outdated when the
|
|
784
821
|
code beneath it changes, but that does not resolve it, so the feedback remains
|
|
785
822
|
unanswered.
|
|
786
823
|
|
|
787
|
-
Any non-zero count is coloured **
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
824
|
+
Any non-zero count is coloured **amber**, one step below the red of `Conflict`,
|
|
825
|
+
`Fail` and `Review`. Automated feedback is worth answering, but nobody is
|
|
826
|
+
waiting on it, and there is far more of it — colouring the two alike would let
|
|
827
|
+
the bot's threads mask the rarer case that actually holds a person up. The count
|
|
828
|
+
still feeds the row ranking, so a backlog awaiting review outranks an untouched
|
|
829
|
+
one of the same size.
|
|
791
830
|
|
|
792
831
|
The scan reads the **newest 20 review threads per pull request**, which is the
|
|
793
832
|
single most expensive part of the prefetch (see the cost figures above). The
|
|
@@ -802,8 +841,8 @@ clear — see below.
|
|
|
802
841
|
#### Automation backlog thresholds
|
|
803
842
|
|
|
804
843
|
On the terminal the counts are coloured so the table reads at a glance: `Human`
|
|
805
|
-
and `Ext` **green**, `Conflict`, `Fail` and `
|
|
806
|
-
coloured against two configured thresholds.
|
|
844
|
+
and `Ext` **green**, `Conflict`, `Fail` and `Review` **red**, `Copilot`
|
|
845
|
+
**amber**, and `Auto` coloured against two configured thresholds.
|
|
807
846
|
|
|
808
847
|
Dependabot stops raising pull requests once a repository reaches its
|
|
809
848
|
`open-pull-requests-limit`, so the repository quietly stops receiving dependency
|
|
@@ -1025,7 +1064,7 @@ reports as an App and any unrecognised login carrying the `[bot]` marker — so
|
|
|
1025
1064
|
future bot is classified as automation rather than mistaken for an outside
|
|
1026
1065
|
contributor.
|
|
1027
1066
|
|
|
1028
|
-
`Fail`, `Conflict` and `Copilot` count only **established** states. GitHub
|
|
1067
|
+
`Fail`, `Conflict`, `Review` and `Copilot` count only **established** states. GitHub
|
|
1029
1068
|
computes mergeability lazily and answers `UNKNOWN` until it settles, and reports
|
|
1030
1069
|
no check rollup at all when no checks have run; neither absence is evidence that
|
|
1031
1070
|
a pull request is ready, so neither is counted either way. `Copilot` follows the
|
|
@@ -1039,10 +1078,15 @@ holding any such reading says so in its description: `Copilot` then
|
|
|
1039
1078
|
**undercounts and should be read as a lower bound**, exactly as `Ext` does when
|
|
1040
1079
|
membership cannot be read.
|
|
1041
1080
|
|
|
1081
|
+
`Review` carries a weaker version of the same hedge, and earns it far less
|
|
1082
|
+
often: `reviewDecision` is exact, so only a pull request GitHub *does* report
|
|
1083
|
+
changes requested on can leave the question open, and then only when the
|
|
1084
|
+
opinionated-review window fails to attribute it to anybody.
|
|
1085
|
+
|
|
1042
1086
|
Repositories with no open pull requests are counted in the footer rather than
|
|
1043
1087
|
listed. Rows rank by total open pull requests, then by those failing,
|
|
1044
|
-
conflicting or awaiting
|
|
1045
|
-
overlap)
|
|
1088
|
+
conflicting or awaiting review — automated or human (counted once each, since
|
|
1089
|
+
the columns overlap) — so two repositories with equal backlogs surface the more
|
|
1046
1090
|
stuck one first.
|
|
1047
1091
|
|
|
1048
1092
|
> **Accuracy note.** As with the issues table, `Total` is exact at any size,
|
|
@@ -692,17 +692,17 @@ organisation](#inside-or-outside-the-organisation):
|
|
|
692
692
|
|
|
693
693
|
```text
|
|
694
694
|
Pull Requests
|
|
695
|
-
|
|
696
|
-
┃ Repository ┃ Human ┃ Ext ┃ Auto ┃ Conflict ┃ Fail ┃ Copilot ┃ Draft ┃ Total ┃
|
|
697
|
-
|
|
698
|
-
│ lftools-uv │ 8 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 8 │
|
|
699
|
-
│ dependamerge │ 3 │ 0 │ 0 │ 0 │ 0 │ 2 │ 0 │ 3 │
|
|
700
|
-
│ gha-workflow-linter │ 2 │ 0 │ 0 │ 0 │ 1 │ 0 │ 1 │ 2 │
|
|
701
|
-
│ harden-runner-block- │ 1 │ 0 │ 0 │ 1 │ 0 │ 0 │ 1 │ 1 │
|
|
702
|
-
│ action │ │ │ │ │ │ │ │ │
|
|
703
|
-
|
|
704
|
-
│ Total │ 14 │ 0 │ 0 │ 1 │ 1 │ 2 │ 2 │ 14 │
|
|
705
|
-
|
|
695
|
+
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━┳━━━━━━┳━━━━━━━━━━┳━━━━━━┳━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━┓
|
|
696
|
+
┃ Repository ┃ Human ┃ Ext ┃ Auto ┃ Conflict ┃ Fail ┃ Review ┃ Copilot ┃ Draft ┃ Total ┃
|
|
697
|
+
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━╇━━━━━━╇━━━━━━━━━━╇━━━━━━╇━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━┩
|
|
698
|
+
│ lftools-uv │ 8 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 8 │
|
|
699
|
+
│ dependamerge │ 3 │ 0 │ 0 │ 0 │ 0 │ 1 │ 2 │ 0 │ 3 │
|
|
700
|
+
│ gha-workflow-linter │ 2 │ 0 │ 0 │ 0 │ 1 │ 0 │ 0 │ 1 │ 2 │
|
|
701
|
+
│ harden-runner-block- │ 1 │ 0 │ 0 │ 1 │ 0 │ 0 │ 0 │ 1 │ 1 │
|
|
702
|
+
│ action │ │ │ │ │ │ │ │ │ │
|
|
703
|
+
├──────────────────────┼───────┼─────┼──────┼──────────┼──────┼────────┼─────────┼───────┼───────┤
|
|
704
|
+
│ Total │ 14 │ 0 │ 0 │ 1 │ 1 │ 1 │ 2 │ 2 │ 14 │
|
|
705
|
+
└──────────────────────┴───────┴─────┴──────┴──────────┴──────┴────────┴─────────┴───────┴───────┘
|
|
706
706
|
… and 9 more
|
|
707
707
|
❌ 13 With open pull requests
|
|
708
708
|
✅ 104 No open pull requests
|
|
@@ -716,34 +716,73 @@ The columns form **two independent groupings**:
|
|
|
716
716
|
| `Ext` | Human pull requests raised from outside the organisation — a **subset of Human**, which is why it sits beside it. |
|
|
717
717
|
| `Conflict` | Blocked on a merge conflict. |
|
|
718
718
|
| `Fail` | Latest checks did not pass. The rollup includes optional checks, so this is not by itself proof that the merge is blocked. |
|
|
719
|
+
| `Review` | A reviewer has **requested changes** — a person waiting on the author. |
|
|
719
720
|
| `Copilot` | Carries at least one **unresolved review thread** opened by GitHub's automated code reviewer. |
|
|
720
721
|
| `Draft` | Marked as a draft. |
|
|
721
722
|
|
|
722
|
-
`Conflict`, `Fail`, `Copilot` and `Draft` **overlap** each other and
|
|
723
|
-
split, so they do not sum to `Total` and are not meant to: one pull
|
|
724
|
-
is conflicting, failing *and* a draft is counted once in each of
|
|
725
|
-
columns, and once under `Human` or `Auto`. Only `Human` + `Auto`
|
|
726
|
-
the collected total. They are ordered worst-first — a conflict
|
|
727
|
-
rebase, a failing check may only need a re-run,
|
|
728
|
-
|
|
729
|
-
all.
|
|
723
|
+
`Conflict`, `Fail`, `Review`, `Copilot` and `Draft` **overlap** each other and
|
|
724
|
+
the author split, so they do not sum to `Total` and are not meant to: one pull
|
|
725
|
+
request that is conflicting, failing *and* a draft is counted once in each of
|
|
726
|
+
those three columns, and once under `Human` or `Auto`. Only `Human` + `Auto`
|
|
727
|
+
reconciles with the collected total. They are ordered worst-first — a conflict
|
|
728
|
+
needs a human to rebase, a failing check may only need a re-run, requested
|
|
729
|
+
changes need the author to act, unresolved automated feedback needs somebody but
|
|
730
|
+
does not hold the merge button down, and a draft is not blocked at all.
|
|
731
|
+
|
|
732
|
+
`Review` and `Copilot` sit together inside the blocker group, and deliberately
|
|
733
|
+
not beside `Human`: that column names who **raised** a pull request, whereas
|
|
734
|
+
these two name who is **waiting on** it. The grouping is what keeps those two
|
|
735
|
+
readings of "human" apart.
|
|
736
|
+
|
|
737
|
+
#### Requested changes
|
|
738
|
+
|
|
739
|
+
`Review` counts pull requests where **a person** has asked for changes and not
|
|
740
|
+
withdrawn it.
|
|
741
|
+
|
|
742
|
+
Two fields answer that, each covering the other's blind spot. `reviewDecision` is
|
|
743
|
+
GitHub's own verdict, computed over every review and accounting for dismissals,
|
|
744
|
+
so it is **exact at any review count** — but it names nobody, and a GitHub App
|
|
745
|
+
holding pull-request write permission can request changes exactly as a person
|
|
746
|
+
can. `latestOpinionatedReviews` names the reviewers, but is a bounded window of
|
|
747
|
+
five (one review per reviewer, so five reviewers rather than five reviews).
|
|
748
|
+
|
|
749
|
+
So the decision **gates** and the window **attributes**. Anything other than
|
|
750
|
+
`CHANGES_REQUESTED` is a definite zero whatever the window holds, and only once
|
|
751
|
+
GitHub says changes are outstanding does the reviewer's identity matter — which
|
|
752
|
+
keeps the cheap exact answer for almost every pull request and pays the window's
|
|
753
|
+
uncertainty on the few where it changes the reading. Automated reviewers are
|
|
754
|
+
recognised by the same rule as the `Auto` column.
|
|
755
|
+
|
|
756
|
+
Only `CHANGES_REQUESTED` gates. **`REVIEW_REQUIRED` is deliberately excluded**:
|
|
757
|
+
it reports that a branch rule demands a review, not that anybody objected, so on
|
|
758
|
+
an organisation that requires review by default it would mark nearly every human
|
|
759
|
+
pull request and distinguish none of them. The rule is written as an allow-list
|
|
760
|
+
of the one blocking state rather than as "anything that is not `APPROVED`", so a
|
|
761
|
+
state GitHub adds later arrives uncounted rather than pre-counted as a blocker.
|
|
762
|
+
|
|
763
|
+
Where a request for changes cannot be attributed — the reviews were unreadable,
|
|
764
|
+
the author is gone, or the window held only automated requests without covering
|
|
765
|
+
every reviewer — the pull request is left **uncounted rather than credited to a
|
|
766
|
+
person**, and the table says so in its description.
|
|
730
767
|
|
|
731
768
|
#### Unresolved Copilot feedback
|
|
732
769
|
|
|
733
|
-
`Copilot` counts the pull requests **waiting on
|
|
734
|
-
review thread counts when it was opened by GitHub's automated code
|
|
735
|
-
`copilot-pull-request-reviewer` bot) and nobody has resolved it.
|
|
736
|
-
identified from each thread's **opening** comment, since later
|
|
737
|
-
usually the human answering the review.
|
|
770
|
+
`Copilot` counts the pull requests **waiting on somebody to answer an automated
|
|
771
|
+
review**. A review thread counts when it was opened by GitHub's automated code
|
|
772
|
+
reviewer (the `copilot-pull-request-reviewer` bot) and nobody has resolved it.
|
|
773
|
+
The reviewer is identified from each thread's **opening** comment, since later
|
|
774
|
+
replies are usually the human answering the review.
|
|
738
775
|
|
|
739
776
|
An **outdated** thread still counts: GitHub marks a thread outdated when the
|
|
740
777
|
code beneath it changes, but that does not resolve it, so the feedback remains
|
|
741
778
|
unanswered.
|
|
742
779
|
|
|
743
|
-
Any non-zero count is coloured **
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
780
|
+
Any non-zero count is coloured **amber**, one step below the red of `Conflict`,
|
|
781
|
+
`Fail` and `Review`. Automated feedback is worth answering, but nobody is
|
|
782
|
+
waiting on it, and there is far more of it — colouring the two alike would let
|
|
783
|
+
the bot's threads mask the rarer case that actually holds a person up. The count
|
|
784
|
+
still feeds the row ranking, so a backlog awaiting review outranks an untouched
|
|
785
|
+
one of the same size.
|
|
747
786
|
|
|
748
787
|
The scan reads the **newest 20 review threads per pull request**, which is the
|
|
749
788
|
single most expensive part of the prefetch (see the cost figures above). The
|
|
@@ -758,8 +797,8 @@ clear — see below.
|
|
|
758
797
|
#### Automation backlog thresholds
|
|
759
798
|
|
|
760
799
|
On the terminal the counts are coloured so the table reads at a glance: `Human`
|
|
761
|
-
and `Ext` **green**, `Conflict`, `Fail` and `
|
|
762
|
-
coloured against two configured thresholds.
|
|
800
|
+
and `Ext` **green**, `Conflict`, `Fail` and `Review` **red**, `Copilot`
|
|
801
|
+
**amber**, and `Auto` coloured against two configured thresholds.
|
|
763
802
|
|
|
764
803
|
Dependabot stops raising pull requests once a repository reaches its
|
|
765
804
|
`open-pull-requests-limit`, so the repository quietly stops receiving dependency
|
|
@@ -981,7 +1020,7 @@ reports as an App and any unrecognised login carrying the `[bot]` marker — so
|
|
|
981
1020
|
future bot is classified as automation rather than mistaken for an outside
|
|
982
1021
|
contributor.
|
|
983
1022
|
|
|
984
|
-
`Fail`, `Conflict` and `Copilot` count only **established** states. GitHub
|
|
1023
|
+
`Fail`, `Conflict`, `Review` and `Copilot` count only **established** states. GitHub
|
|
985
1024
|
computes mergeability lazily and answers `UNKNOWN` until it settles, and reports
|
|
986
1025
|
no check rollup at all when no checks have run; neither absence is evidence that
|
|
987
1026
|
a pull request is ready, so neither is counted either way. `Copilot` follows the
|
|
@@ -995,10 +1034,15 @@ holding any such reading says so in its description: `Copilot` then
|
|
|
995
1034
|
**undercounts and should be read as a lower bound**, exactly as `Ext` does when
|
|
996
1035
|
membership cannot be read.
|
|
997
1036
|
|
|
1037
|
+
`Review` carries a weaker version of the same hedge, and earns it far less
|
|
1038
|
+
often: `reviewDecision` is exact, so only a pull request GitHub *does* report
|
|
1039
|
+
changes requested on can leave the question open, and then only when the
|
|
1040
|
+
opinionated-review window fails to attribute it to anybody.
|
|
1041
|
+
|
|
998
1042
|
Repositories with no open pull requests are counted in the footer rather than
|
|
999
1043
|
listed. Rows rank by total open pull requests, then by those failing,
|
|
1000
|
-
conflicting or awaiting
|
|
1001
|
-
overlap)
|
|
1044
|
+
conflicting or awaiting review — automated or human (counted once each, since
|
|
1045
|
+
the columns overlap) — so two repositories with equal backlogs surface the more
|
|
1002
1046
|
stuck one first.
|
|
1003
1047
|
|
|
1004
1048
|
> **Accuracy note.** As with the issues table, `Total` is exact at any size,
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.15.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 15, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -311,9 +311,11 @@ _CATEGORIES: dict[CategoryKey, CategoryMeta] = {
|
|
|
311
311
|
"Conflict counts pull requests blocked on a merge conflict; Fail "
|
|
312
312
|
"counts those whose latest checks did not pass, which includes "
|
|
313
313
|
"optional checks and so is not by itself proof that a merge is "
|
|
314
|
-
"blocked;
|
|
315
|
-
"
|
|
316
|
-
"
|
|
314
|
+
"blocked; Review counts those a reviewer has asked for changes on, "
|
|
315
|
+
"which is a person waiting on the author; Copilot counts those "
|
|
316
|
+
"still carrying an unresolved review thread opened by GitHub's "
|
|
317
|
+
"automated code reviewer; Draft counts "
|
|
318
|
+
"those still marked as drafts. Those five "
|
|
317
319
|
"are independent of the author split and of each other, so one "
|
|
318
320
|
"pull request can appear in more than one of them. Ranked by total "
|
|
319
321
|
"open pull requests, then by those failing, conflicting or "
|
|
@@ -17,6 +17,7 @@ from typing import cast
|
|
|
17
17
|
import httpx
|
|
18
18
|
|
|
19
19
|
from github_security_report.client.copilot import _copilot_unresolved
|
|
20
|
+
from github_security_report.client.reviews import _changes_requested
|
|
20
21
|
from github_security_report.models import (
|
|
21
22
|
AuthorRef,
|
|
22
23
|
IssueRef,
|
|
@@ -341,6 +342,7 @@ def _pull_request_ref(node: object) -> PullRequestRef | None:
|
|
|
341
342
|
conflicting=conflicting,
|
|
342
343
|
failing=_check_rollup_failed(node),
|
|
343
344
|
copilot_unresolved=_copilot_unresolved(node),
|
|
345
|
+
changes_requested=_changes_requested(node),
|
|
344
346
|
)
|
|
345
347
|
|
|
346
348
|
|
|
@@ -111,6 +111,17 @@ _PULL_REQUEST_WINDOW = 25
|
|
|
111
111
|
# truncate, and the assignment breakdown is therefore exact for every collected
|
|
112
112
|
# pull request.
|
|
113
113
|
_ASSIGNEE_WINDOW = 10
|
|
114
|
+
# Opinionated reviews inspected per pull request, to tell whose request for
|
|
115
|
+
# changes is outstanding. ``reviewDecision`` says *that* changes are requested
|
|
116
|
+
# but names nobody, and a GitHub App with pull-request write permission can
|
|
117
|
+
# request them exactly as a person can -- so identity needs its own field.
|
|
118
|
+
#
|
|
119
|
+
# The connection returns at most one review per reviewer, so five covers a pull
|
|
120
|
+
# request with five opinionated reviewers rather than five reviews. Measured
|
|
121
|
+
# against a 25-repository batch this adds 6 points, which is the cheapest of the
|
|
122
|
+
# three windows here by an order of magnitude, and ``totalCount`` rides along
|
|
123
|
+
# free to keep it honest.
|
|
124
|
+
_OPINIONATED_REVIEW_WINDOW = 5
|
|
114
125
|
# Review threads inspected per pull request, to spot outstanding Copilot review
|
|
115
126
|
# feedback. This is the most expensive window in the fragment, because it
|
|
116
127
|
# multiplies the pull-request window and then again by the one comment needed to
|
|
@@ -179,9 +190,14 @@ fragment RepoData on Repository {{
|
|
|
179
190
|
number
|
|
180
191
|
isDraft
|
|
181
192
|
mergeable
|
|
193
|
+
reviewDecision
|
|
182
194
|
authorAssociation
|
|
183
195
|
author {{ __typename login }}
|
|
184
196
|
assignees(first: {_ASSIGNEE_WINDOW}) {{ nodes {{ login }} }}
|
|
197
|
+
latestOpinionatedReviews(first: {_OPINIONATED_REVIEW_WINDOW}) {{
|
|
198
|
+
totalCount
|
|
199
|
+
nodes {{ state author {{ __typename login }} }}
|
|
200
|
+
}}
|
|
185
201
|
reviewThreads(last: {_REVIEW_THREAD_WINDOW}) {{
|
|
186
202
|
totalCount
|
|
187
203
|
nodes {{
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 The Linux Foundation
|
|
3
|
+
"""Reading an outstanding request for changes from a pull-request node.
|
|
4
|
+
|
|
5
|
+
One question, asked of the two review fields the batched prefetch requests: is a
|
|
6
|
+
*person* waiting on this pull request's author? Kept apart from the general node
|
|
7
|
+
parsing for the same reason as the Copilot reading beside it -- it has to reason
|
|
8
|
+
about a bounded window, and so about what that window's edge does and does not
|
|
9
|
+
prove.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from github_security_report.authors import is_automation_author
|
|
15
|
+
|
|
16
|
+
# The one review decision that means somebody objected. An allow-list of that
|
|
17
|
+
# single state rather than "anything that is not APPROVED": REVIEW_REQUIRED says
|
|
18
|
+
# a branch rule demands a review, not that a reviewer objected, so counting it
|
|
19
|
+
# would mark nearly every pull request in an organisation that requires review by
|
|
20
|
+
# default. Writing the rule positively also means a state GitHub adds later
|
|
21
|
+
# arrives *uncounted* rather than pre-counted as a blocker.
|
|
22
|
+
_BLOCKING_DECISION = "CHANGES_REQUESTED"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _changes_requested(node: dict) -> bool | None:
|
|
26
|
+
"""Whether a *person* has asked for changes and not withdrawn it.
|
|
27
|
+
|
|
28
|
+
Two fields, each covering the other's blind spot. ``reviewDecision`` is
|
|
29
|
+
GitHub's own verdict, computed over every review and accounting for
|
|
30
|
+
dismissals, so it is exact however long the review ran -- but it names
|
|
31
|
+
nobody, and a GitHub App holding pull-request write permission can request
|
|
32
|
+
changes exactly as a person can. ``latestOpinionatedReviews`` names the
|
|
33
|
+
reviewers but is a bounded window.
|
|
34
|
+
|
|
35
|
+
So the decision *gates* and the window *attributes*. Anything other than
|
|
36
|
+
``CHANGES_REQUESTED`` is a definite False whatever the window holds, and
|
|
37
|
+
only once GitHub says changes are outstanding does identity matter. That
|
|
38
|
+
keeps the cheap exact answer for the overwhelming majority of pull requests
|
|
39
|
+
and pays the window's uncertainty on the few where it changes the reading.
|
|
40
|
+
|
|
41
|
+
``None`` is the open question: the fields could not be read, a requested
|
|
42
|
+
change carried no author to attribute it to, or the window held only
|
|
43
|
+
automated requests without covering every opinionated reviewer -- in which
|
|
44
|
+
case a person's may sit among the reviews this run never saw. As elsewhere,
|
|
45
|
+
None is "not established" rather than "nothing outstanding".
|
|
46
|
+
"""
|
|
47
|
+
if "reviewDecision" not in node:
|
|
48
|
+
return None
|
|
49
|
+
decision = node["reviewDecision"]
|
|
50
|
+
if not isinstance(decision, str) or decision != _BLOCKING_DECISION:
|
|
51
|
+
return False
|
|
52
|
+
reviews = node.get("latestOpinionatedReviews")
|
|
53
|
+
if not isinstance(reviews, dict):
|
|
54
|
+
return None
|
|
55
|
+
nodes = reviews.get("nodes")
|
|
56
|
+
if not isinstance(nodes, list):
|
|
57
|
+
return None
|
|
58
|
+
seen = 0
|
|
59
|
+
for review in nodes:
|
|
60
|
+
if not isinstance(review, dict):
|
|
61
|
+
continue
|
|
62
|
+
seen += 1
|
|
63
|
+
if review.get("state") != _BLOCKING_DECISION:
|
|
64
|
+
continue
|
|
65
|
+
author = review.get("author")
|
|
66
|
+
if not isinstance(author, dict):
|
|
67
|
+
# A request for changes we cannot attribute -- a deleted account,
|
|
68
|
+
# say. Counting it would guess at a person; skipping it would let
|
|
69
|
+
# the loop fall through to a confident False.
|
|
70
|
+
return None
|
|
71
|
+
if not is_automation_author(author.get("login"), author.get("__typename")):
|
|
72
|
+
return True
|
|
73
|
+
total = reviews.get("totalCount")
|
|
74
|
+
if not isinstance(total, int) or isinstance(total, bool) or total > seen:
|
|
75
|
+
return None
|
|
76
|
+
return False
|
{github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/models.py
RENAMED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
Encodes the Phase 0 design (see ``docs/BRIEF.md`` and
|
|
6
6
|
``docs/phase0-findings.md``): the six ranked signals, the four-state per-report
|
|
7
|
-
classification, severity counts with hierarchical worst-first ordering
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
classification, and severity counts with hierarchical worst-first ordering.
|
|
8
|
+
The rules for ordering offenders *between* repositories live in ``ranking.py``,
|
|
9
|
+
which reads these models rather than being part of them.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -178,7 +178,7 @@ class IssueRef:
|
|
|
178
178
|
class PullRequestRef:
|
|
179
179
|
"""One open pull request's review-load facts.
|
|
180
180
|
|
|
181
|
-
``draft`` and the
|
|
181
|
+
``draft`` and the four blocked flags are independent of the author and of
|
|
182
182
|
each other, so one pull request may be a draft *and* conflicting *and*
|
|
183
183
|
awaiting review; the table counts each axis separately rather than bucketing
|
|
184
184
|
rows.
|
|
@@ -207,6 +207,24 @@ class PullRequestRef:
|
|
|
207
207
|
# the threads this run never saw. As with ``conflicting`` and ``failing``,
|
|
208
208
|
# None is "not established" rather than "nothing outstanding".
|
|
209
209
|
copilot_unresolved: bool | None = None
|
|
210
|
+
# True when a *person* has requested changes and not withdrawn it. Read
|
|
211
|
+
# from ``reviewDecision``, which GitHub computes over every review and so is
|
|
212
|
+
# exact at any review count, attributed through the bounded window of
|
|
213
|
+
# opinionated reviews -- the decision names nobody, and a GitHub App can
|
|
214
|
+
# request changes exactly as a person can.
|
|
215
|
+
#
|
|
216
|
+
# None where that attribution could not be settled: the fields were
|
|
217
|
+
# unreadable, a request carried no author, or the window held only automated
|
|
218
|
+
# requests without covering every reviewer. As with the flags above, None is
|
|
219
|
+
# "not established" rather than "nothing outstanding" -- though it is far
|
|
220
|
+
# rarer here, since it needs GitHub to report changes requested *and* the
|
|
221
|
+
# window to fall short.
|
|
222
|
+
#
|
|
223
|
+
# Only CHANGES_REQUESTED counts. REVIEW_REQUIRED is deliberately excluded:
|
|
224
|
+
# it reports that a branch rule demands a review, not that anyone objected,
|
|
225
|
+
# so on an organisation that requires review by default it would mark almost
|
|
226
|
+
# every human pull request and say nothing about any of them.
|
|
227
|
+
changes_requested: bool | None = None
|
|
210
228
|
|
|
211
229
|
|
|
212
230
|
@dataclass
|
|
@@ -367,70 +385,3 @@ class RepoSignal:
|
|
|
367
385
|
@property
|
|
368
386
|
def is_offender(self) -> bool:
|
|
369
387
|
return self.state is RepoState.OFFENDER
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
# The rungs eligible to lead the Scorecard ordering, worst-first.
|
|
373
|
-
# ``INFORMATIONAL`` is deliberately absent: it is the non-actionable rung, so it
|
|
374
|
-
# never displaces the score as the primary key.
|
|
375
|
-
LEAD_RUNGS: tuple[Severity, ...] = tuple(
|
|
376
|
-
rung for rung in RUNGS_WORST_FIRST if rung is not Severity.INFORMATIONAL
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
def lead_rung(offenders: list[RepoSignal]) -> Severity | None:
|
|
381
|
-
"""The worst severity rung any offender actually carries.
|
|
382
|
-
|
|
383
|
-
Returns ``None`` when no offender carries a finding at Low or above, in
|
|
384
|
-
which case there is no severity tier worth leading on.
|
|
385
|
-
"""
|
|
386
|
-
return next(
|
|
387
|
-
(rung for rung in LEAD_RUNGS if any(s.counts.at(rung) for s in offenders)),
|
|
388
|
-
None,
|
|
389
|
-
)
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
def rank_offenders(signals: list[RepoSignal]) -> list[RepoSignal]:
|
|
393
|
-
"""Sort offenders worst-first for a single signal.
|
|
394
|
-
|
|
395
|
-
Alert-based signals sort by the hierarchical severity key descending, with
|
|
396
|
-
total as a tiebreaker.
|
|
397
|
-
|
|
398
|
-
Scorecard sorts on two tiers: the count at the worst severity rung present
|
|
399
|
-
anywhere in the table (descending), then the aggregate score (ascending,
|
|
400
|
-
lower == worse). The leading rung cascades -- Critical, else High, else
|
|
401
|
-
Medium, else Low -- so the rung that actually discriminates between
|
|
402
|
-
repositories leads, and a lone Critical can never be buried mid-table by a
|
|
403
|
-
weaker repository with a lower score. When no offender carries a finding at
|
|
404
|
-
Low or above, the score alone orders the table.
|
|
405
|
-
|
|
406
|
-
Repo name breaks remaining ties, ascending.
|
|
407
|
-
|
|
408
|
-
Numeric components are negated so the whole sort runs ascending (no
|
|
409
|
-
``reverse=True``); that keeps the name tiebreaker correctly ascending even
|
|
410
|
-
when one name is a prefix of another.
|
|
411
|
-
"""
|
|
412
|
-
offenders = [s for s in signals if s.is_offender]
|
|
413
|
-
if not offenders:
|
|
414
|
-
return []
|
|
415
|
-
signal = offenders[0].signal
|
|
416
|
-
if signal.sort_ascending:
|
|
417
|
-
rung = lead_rung(offenders)
|
|
418
|
-
return sorted(
|
|
419
|
-
offenders,
|
|
420
|
-
key=lambda s: (
|
|
421
|
-
-s.counts.at(rung) if rung is not None else 0,
|
|
422
|
-
s.score if s.score is not None else float("inf"),
|
|
423
|
-
s.repo.name,
|
|
424
|
-
),
|
|
425
|
-
)
|
|
426
|
-
return sorted(
|
|
427
|
-
offenders,
|
|
428
|
-
key=lambda s: (
|
|
429
|
-
-s.counts.critical,
|
|
430
|
-
-s.counts.high,
|
|
431
|
-
-s.counts.medium,
|
|
432
|
-
-s.counts.low,
|
|
433
|
-
-s.counts.total,
|
|
434
|
-
s.repo.name,
|
|
435
|
-
),
|
|
436
|
-
)
|
|
@@ -30,7 +30,7 @@ whose rendered columns vary by surface and by data (Slack abbreviates the
|
|
|
30
30
|
headers and drops Total, and the Informational column appears only when
|
|
31
31
|
populated), so resolving against a renderer's column list would make the
|
|
32
32
|
ordering depend on which surface asked. Omitting ``sort`` keeps
|
|
33
|
-
:func:`
|
|
33
|
+
:func:`ranking.rank_offenders`, whose default encodes domain logic a column sort
|
|
34
34
|
cannot express -- notably Scorecard's cascade through the worst populated
|
|
35
35
|
severity rung, which stops a lone Critical being buried by a weaker repository
|
|
36
36
|
with a lower score.
|
|
@@ -417,7 +417,7 @@ def apply_configured_signal_order(
|
|
|
417
417
|
) -> None:
|
|
418
418
|
"""Reorder each signal's offenders in place using its configured ordering.
|
|
419
419
|
|
|
420
|
-
A signal with no ``sort`` keeps :func:`
|
|
420
|
+
A signal with no ``sort`` keeps :func:`ranking.rank_offenders`, whose default
|
|
421
421
|
is not expressible as a column list: Scorecard cascades through the worst
|
|
422
422
|
severity rung any offender actually carries, which is a property of the
|
|
423
423
|
table as a whole rather than of any one row.
|
|
@@ -71,6 +71,7 @@ from github_security_report.pulls.columns import (
|
|
|
71
71
|
OTHERS_ROW,
|
|
72
72
|
PERSONAL_ASSIGNMENT_ROWS,
|
|
73
73
|
REPOSITORY_COLUMN,
|
|
74
|
+
REVIEW_COLUMN,
|
|
74
75
|
TOTAL_COLUMN,
|
|
75
76
|
TRUNCATED_MARKER,
|
|
76
77
|
UNASSIGNED_ROW,
|
|
@@ -84,6 +85,7 @@ from github_security_report.pulls.counting import (
|
|
|
84
85
|
copilot_indeterminate,
|
|
85
86
|
count_pull_requests,
|
|
86
87
|
is_mine,
|
|
88
|
+
review_indeterminate,
|
|
87
89
|
)
|
|
88
90
|
from github_security_report.pulls.presentation import (
|
|
89
91
|
_cell_levels,
|
|
@@ -131,6 +133,7 @@ __all__ = [
|
|
|
131
133
|
"PERSONAL_ASSIGNMENT_ROWS",
|
|
132
134
|
"PullRequestRef",
|
|
133
135
|
"REPOSITORY_COLUMN",
|
|
136
|
+
"REVIEW_COLUMN",
|
|
134
137
|
"Repo",
|
|
135
138
|
"RepoGraphData",
|
|
136
139
|
"Set",
|
|
@@ -151,4 +154,5 @@ __all__ = [
|
|
|
151
154
|
"is_automation_author",
|
|
152
155
|
"is_external_author",
|
|
153
156
|
"is_mine",
|
|
157
|
+
"review_indeterminate",
|
|
154
158
|
]
|
|
@@ -15,6 +15,7 @@ DRAFT_COLUMN = "Draft"
|
|
|
15
15
|
EXTERNAL_COLUMN = "Ext"
|
|
16
16
|
FAILING_COLUMN = "Fail"
|
|
17
17
|
CONFLICT_COLUMN = "Conflict"
|
|
18
|
+
REVIEW_COLUMN = "Review"
|
|
18
19
|
COPILOT_COLUMN = "Copilot"
|
|
19
20
|
TOTAL_COLUMN = "Total"
|
|
20
21
|
|
|
@@ -22,15 +23,21 @@ TOTAL_COLUMN = "Total"
|
|
|
22
23
|
# Ordered so related columns read together: the author split first, with Ext
|
|
23
24
|
# beside Human because it qualifies it (Ext is a subset of Human, never of
|
|
24
25
|
# Auto), then the blockers, worst first -- a conflict needs a human to rebase,
|
|
25
|
-
# a failing check may only need a re-run,
|
|
26
|
-
#
|
|
27
|
-
# all.
|
|
26
|
+
# a failing check may only need a re-run, a reviewer who requested changes needs
|
|
27
|
+
# the author to act, unresolved Copilot feedback needs somebody but does not
|
|
28
|
+
# hold the merge button down, and a draft is not blocked at all.
|
|
29
|
+
#
|
|
30
|
+
# Review sits beside Copilot because the two answer the same question about
|
|
31
|
+
# different reviewers, and both sit inside the blocker group rather than beside
|
|
32
|
+
# Human -- which names who *raised* a pull request, not who reviewed it. The
|
|
33
|
+
# grouping is what keeps those two readings of "human" apart.
|
|
28
34
|
BREAKDOWN_COLUMNS = (
|
|
29
35
|
HUMAN_COLUMN,
|
|
30
36
|
EXTERNAL_COLUMN,
|
|
31
37
|
AUTOMATION_COLUMN,
|
|
32
38
|
CONFLICT_COLUMN,
|
|
33
39
|
FAILING_COLUMN,
|
|
40
|
+
REVIEW_COLUMN,
|
|
34
41
|
COPILOT_COLUMN,
|
|
35
42
|
DRAFT_COLUMN,
|
|
36
43
|
)
|
|
@@ -25,6 +25,7 @@ from github_security_report.pulls.columns import (
|
|
|
25
25
|
HUMAN_COLUMN,
|
|
26
26
|
MINE_ROW,
|
|
27
27
|
OTHERS_ROW,
|
|
28
|
+
REVIEW_COLUMN,
|
|
28
29
|
UNASSIGNED_ROW,
|
|
29
30
|
)
|
|
30
31
|
|
|
@@ -86,6 +87,12 @@ def count_pull_requests(
|
|
|
86
87
|
counts[CONFLICT_COLUMN] += 1
|
|
87
88
|
if pull.copilot_unresolved is True:
|
|
88
89
|
counts[COPILOT_COLUMN] += 1
|
|
90
|
+
# Review needs the same hedge, though it earns it far less often: the
|
|
91
|
+
# decision is exact, so only a pull request GitHub *does* report changes
|
|
92
|
+
# requested on can leave the question open, and then only if the window
|
|
93
|
+
# failed to attribute it.
|
|
94
|
+
if pull.changes_requested is True:
|
|
95
|
+
counts[REVIEW_COLUMN] += 1
|
|
89
96
|
return counts
|
|
90
97
|
|
|
91
98
|
|
|
@@ -148,6 +155,17 @@ def copilot_indeterminate(pulls: tuple[PullRequestRef, ...]) -> bool:
|
|
|
148
155
|
return any(pull.copilot_unresolved is None for pull in pulls)
|
|
149
156
|
|
|
150
157
|
|
|
158
|
+
def review_indeterminate(pulls: tuple[PullRequestRef, ...]) -> bool:
|
|
159
|
+
"""Whether any of these pull requests left the Review question unsettled.
|
|
160
|
+
|
|
161
|
+
Same rule as :func:`copilot_indeterminate` and for the same reason -- a
|
|
162
|
+
bounded window, not a moment that will settle itself -- but reached far
|
|
163
|
+
less often, since it needs GitHub to report changes requested *and* the
|
|
164
|
+
window of opinionated reviews to fail to attribute them to anybody.
|
|
165
|
+
"""
|
|
166
|
+
return any(pull.changes_requested is None for pull in pulls)
|
|
167
|
+
|
|
168
|
+
|
|
151
169
|
def _blocked_count(pulls: tuple[PullRequestRef, ...]) -> int:
|
|
152
170
|
"""Pull requests that are failing, conflicting *or* awaiting Copilot, once each.
|
|
153
171
|
|
|
@@ -157,10 +175,11 @@ def _blocked_count(pulls: tuple[PullRequestRef, ...]) -> int:
|
|
|
157
175
|
stuck ones. The table already says the columns overlap; the ranking has
|
|
158
176
|
to agree with it.
|
|
159
177
|
|
|
160
|
-
Unresolved Copilot feedback
|
|
161
|
-
fact as the other two -- work the
|
|
162
|
-
|
|
163
|
-
|
|
178
|
+
Unresolved Copilot feedback and a reviewer's requested changes join the
|
|
179
|
+
union because they are the same kind of fact as the other two -- work the
|
|
180
|
+
pull request is waiting on somebody for -- and the table already colours
|
|
181
|
+
both as blocking. Leaving them out would rank a repository whose whole
|
|
182
|
+
backlog is awaiting review below an untouched one.
|
|
164
183
|
"""
|
|
165
184
|
return sum(
|
|
166
185
|
1
|
|
@@ -168,4 +187,5 @@ def _blocked_count(pulls: tuple[PullRequestRef, ...]) -> int:
|
|
|
168
187
|
if pull.failing is True
|
|
169
188
|
or pull.conflicting is True
|
|
170
189
|
or pull.copilot_unresolved is True
|
|
190
|
+
or pull.changes_requested is True
|
|
171
191
|
)
|
|
@@ -19,6 +19,7 @@ from github_security_report.pulls.columns import (
|
|
|
19
19
|
EXTERNAL_COLUMN,
|
|
20
20
|
FAILING_COLUMN,
|
|
21
21
|
HUMAN_COLUMN,
|
|
22
|
+
REVIEW_COLUMN,
|
|
22
23
|
TRUNCATED_MARKER,
|
|
23
24
|
)
|
|
24
25
|
from github_security_report.report import CELL_BAD, CELL_GOOD, CELL_WARN
|
|
@@ -68,6 +69,13 @@ def _cell_levels(
|
|
|
68
69
|
|
|
69
70
|
The trailing Total is never emphasised: it is the sum of columns that
|
|
70
71
|
disagree about what good looks like, so no one colour is true of it.
|
|
72
|
+
|
|
73
|
+
The blocked columns are not all one severity. Conflict, Fail and Review are
|
|
74
|
+
red: each needs a person to do something before the pull request can move.
|
|
75
|
+
Copilot is amber, a deliberate step down -- automated review feedback is
|
|
76
|
+
worth answering, but it is not a human waiting on a reply, and colouring the
|
|
77
|
+
two identically would let the louder, more numerous bot threads mask the
|
|
78
|
+
rarer case that actually holds somebody up.
|
|
71
79
|
"""
|
|
72
80
|
levels: list[str | None] = []
|
|
73
81
|
for column in BREAKDOWN_COLUMNS:
|
|
@@ -82,8 +90,10 @@ def _cell_levels(
|
|
|
82
90
|
)
|
|
83
91
|
elif column in (HUMAN_COLUMN, EXTERNAL_COLUMN):
|
|
84
92
|
levels.append(CELL_GOOD if value else None)
|
|
85
|
-
elif column in (CONFLICT_COLUMN, FAILING_COLUMN,
|
|
93
|
+
elif column in (CONFLICT_COLUMN, FAILING_COLUMN, REVIEW_COLUMN):
|
|
86
94
|
levels.append(CELL_BAD if value else None)
|
|
95
|
+
elif column == COPILOT_COLUMN:
|
|
96
|
+
levels.append(CELL_WARN if value else None)
|
|
87
97
|
else:
|
|
88
98
|
# Draft is neither good nor bad: a draft is not blocked, it is
|
|
89
99
|
# simply not finished, so it carries no emphasis.
|
|
@@ -98,6 +108,7 @@ def _describe(
|
|
|
98
108
|
*,
|
|
99
109
|
filtered: bool,
|
|
100
110
|
copilot_partial: bool = False,
|
|
111
|
+
review_partial: bool = False,
|
|
101
112
|
) -> str:
|
|
102
113
|
"""Extend the category description with the caveats the table earned.
|
|
103
114
|
|
|
@@ -146,4 +157,13 @@ def _describe(
|
|
|
146
157
|
"answered, so Copilot undercounts and should be read as a lower "
|
|
147
158
|
"bound."
|
|
148
159
|
)
|
|
160
|
+
if review_partial:
|
|
161
|
+
description += (
|
|
162
|
+
" Who asked for changes could not be settled for at least one pull "
|
|
163
|
+
"request, whose opinionated reviews were unreadable, carried no "
|
|
164
|
+
"author to attribute a request to, or held only automated requests "
|
|
165
|
+
"without covering every reviewer. Such a pull request is left "
|
|
166
|
+
"uncounted rather than credited to a person, so Review undercounts "
|
|
167
|
+
"and should be read as a lower bound."
|
|
168
|
+
)
|
|
149
169
|
return description
|
|
@@ -24,6 +24,7 @@ from github_security_report.pulls.counting import (
|
|
|
24
24
|
copilot_indeterminate,
|
|
25
25
|
count_pull_requests,
|
|
26
26
|
is_mine,
|
|
27
|
+
review_indeterminate,
|
|
27
28
|
)
|
|
28
29
|
from github_security_report.pulls.presentation import (
|
|
29
30
|
_cell_levels,
|
|
@@ -57,6 +58,7 @@ def _build_table(
|
|
|
57
58
|
clean_count = 0
|
|
58
59
|
unknown_count = 0
|
|
59
60
|
copilot_partial = False
|
|
61
|
+
review_partial = False
|
|
60
62
|
footer_labels = assignment_rows(viewer) if footer else ()
|
|
61
63
|
for repo in repos:
|
|
62
64
|
data = graph.get(repo.name, RepoGraphData())
|
|
@@ -91,6 +93,7 @@ def _build_table(
|
|
|
91
93
|
counts = count_pull_requests(selected, members)
|
|
92
94
|
blocked = _blocked_count(selected)
|
|
93
95
|
copilot_partial = copilot_partial or copilot_indeterminate(selected)
|
|
96
|
+
review_partial = review_partial or review_indeterminate(selected)
|
|
94
97
|
cells = (
|
|
95
98
|
*(str(counts[column]) for column in BREAKDOWN_COLUMNS),
|
|
96
99
|
_total_cell(total, truncated),
|
|
@@ -129,6 +132,7 @@ def _build_table(
|
|
|
129
132
|
members,
|
|
130
133
|
filtered=select is not None,
|
|
131
134
|
copilot_partial=copilot_partial,
|
|
135
|
+
review_partial=review_partial,
|
|
132
136
|
)
|
|
133
137
|
return TableSection(
|
|
134
138
|
category=meta,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 The Linux Foundation
|
|
3
|
+
"""Worst-first ordering of the repositories a signal found something in.
|
|
4
|
+
|
|
5
|
+
The domain default behind every offender table: which repository a reader meets
|
|
6
|
+
first. ``ordering.py`` owns the *configurable* orderings a report may ask for
|
|
7
|
+
and defers to :func:`rank_offenders` when a signal names none, so the two are
|
|
8
|
+
separate concerns -- what the reader chose, and what the domain says when they
|
|
9
|
+
chose nothing.
|
|
10
|
+
|
|
11
|
+
Kept out of ``models.py`` because these are decisions taken *about* the models
|
|
12
|
+
rather than part of them, and out of ``severity.py`` because they read a
|
|
13
|
+
``RepoSignal``, which would make that module import the models that already
|
|
14
|
+
import it.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from github_security_report.models import RepoSignal
|
|
20
|
+
from github_security_report.severity import RUNGS_WORST_FIRST, Severity
|
|
21
|
+
|
|
22
|
+
# The rungs eligible to lead the Scorecard ordering, worst-first.
|
|
23
|
+
# ``INFORMATIONAL`` is deliberately absent: it is the non-actionable rung, so it
|
|
24
|
+
# never displaces the score as the primary key.
|
|
25
|
+
LEAD_RUNGS: tuple[Severity, ...] = tuple(
|
|
26
|
+
rung for rung in RUNGS_WORST_FIRST if rung is not Severity.INFORMATIONAL
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def lead_rung(offenders: list[RepoSignal]) -> Severity | None:
|
|
31
|
+
"""The worst severity rung any offender actually carries.
|
|
32
|
+
|
|
33
|
+
Returns ``None`` when no offender carries a finding at Low or above, in
|
|
34
|
+
which case there is no severity tier worth leading on.
|
|
35
|
+
"""
|
|
36
|
+
return next(
|
|
37
|
+
(rung for rung in LEAD_RUNGS if any(s.counts.at(rung) for s in offenders)),
|
|
38
|
+
None,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def rank_offenders(signals: list[RepoSignal]) -> list[RepoSignal]:
|
|
43
|
+
"""Sort offenders worst-first for a single signal.
|
|
44
|
+
|
|
45
|
+
Alert-based signals sort by the hierarchical severity key descending, with
|
|
46
|
+
total as a tiebreaker.
|
|
47
|
+
|
|
48
|
+
Scorecard sorts on two tiers: the count at the worst severity rung present
|
|
49
|
+
anywhere in the table (descending), then the aggregate score (ascending,
|
|
50
|
+
lower == worse). The leading rung cascades -- Critical, else High, else
|
|
51
|
+
Medium, else Low -- so the rung that actually discriminates between
|
|
52
|
+
repositories leads, and a lone Critical can never be buried mid-table by a
|
|
53
|
+
weaker repository with a lower score. When no offender carries a finding at
|
|
54
|
+
Low or above, the score alone orders the table.
|
|
55
|
+
|
|
56
|
+
Repo name breaks remaining ties, ascending.
|
|
57
|
+
|
|
58
|
+
Numeric components are negated so the whole sort runs ascending (no
|
|
59
|
+
``reverse=True``); that keeps the name tiebreaker correctly ascending even
|
|
60
|
+
when one name is a prefix of another.
|
|
61
|
+
"""
|
|
62
|
+
offenders = [s for s in signals if s.is_offender]
|
|
63
|
+
if not offenders:
|
|
64
|
+
return []
|
|
65
|
+
signal = offenders[0].signal
|
|
66
|
+
if signal.sort_ascending:
|
|
67
|
+
rung = lead_rung(offenders)
|
|
68
|
+
return sorted(
|
|
69
|
+
offenders,
|
|
70
|
+
key=lambda s: (
|
|
71
|
+
-s.counts.at(rung) if rung is not None else 0,
|
|
72
|
+
s.score if s.score is not None else float("inf"),
|
|
73
|
+
s.repo.name,
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
return sorted(
|
|
77
|
+
offenders,
|
|
78
|
+
key=lambda s: (
|
|
79
|
+
-s.counts.critical,
|
|
80
|
+
-s.counts.high,
|
|
81
|
+
-s.counts.medium,
|
|
82
|
+
-s.counts.low,
|
|
83
|
+
-s.counts.total,
|
|
84
|
+
s.repo.name,
|
|
85
|
+
),
|
|
86
|
+
)
|
|
@@ -37,7 +37,7 @@ from github_security_report.models import RepoSignal as RepoSignal
|
|
|
37
37
|
from github_security_report.models import RepoState as RepoState
|
|
38
38
|
from github_security_report.models import SeverityCounts as SeverityCounts
|
|
39
39
|
from github_security_report.models import SignalType as SignalType
|
|
40
|
-
from github_security_report.
|
|
40
|
+
from github_security_report.ranking import rank_offenders as rank_offenders
|
|
41
41
|
from github_security_report.report.aggregate import (
|
|
42
42
|
SIGNAL_ORDER,
|
|
43
43
|
OrgReport,
|
|
@@ -22,8 +22,8 @@ from github_security_report.models import (
|
|
|
22
22
|
RepoSignal,
|
|
23
23
|
RepoState,
|
|
24
24
|
SignalType,
|
|
25
|
-
rank_offenders,
|
|
26
25
|
)
|
|
26
|
+
from github_security_report.ranking import rank_offenders
|
|
27
27
|
from github_security_report.report.signals import SignalSection
|
|
28
28
|
from github_security_report.report.tables import TableSection
|
|
29
29
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/gating.py
RENAMED
|
File without changes
|
{github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/gitctx.py
RENAMED
|
File without changes
|
{github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/issues.py
RENAMED
|
File without changes
|
{github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/layout.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/runner.py
RENAMED
|
File without changes
|
{github_security_report-0.14.2 → github_security_report-0.15.0}/src/github_security_report/scope.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|