Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f9c35c5
fix(ci): keep a branch name out of the shell it is used in
donislawdev Aug 18, 2026
eddaa6f
ci: pin every action to a commit, and let a new release cool down
donislawdev Aug 18, 2026
74cbd24
chore: answer the last three scanner findings, two by fixing one thing
donislawdev Aug 18, 2026
3c9a94e
chore: adopt ruff, and close the 56 findings it opens with
donislawdev Aug 18, 2026
aaf941c
ci: run ruff on every pull request, blocking on F and B
donislawdev Aug 18, 2026
61a328e
chore: adopt mypy, and answer the thirty things it says
donislawdev Aug 18, 2026
eae348c
ci: type check the package on every pull request
donislawdev Aug 18, 2026
2327d33
ci: scan with semgrep on every pull request, and gate it ourselves
donislawdev Aug 18, 2026
ede3333
feat(release): sign where the download came from, not just what is in it
donislawdev Aug 19, 2026
f8701c5
feat(deps): pin the bytes of the runtime dependencies, not just the v…
donislawdev Aug 19, 2026
e5e49be
feat(build): pin the freezer's whole chain, not just the freezer
donislawdev Aug 19, 2026
1af3127
ci: audit the pinned dependencies weekly, and open an issue on a finding
donislawdev Aug 19, 2026
5ddc177
ci: let an outside grader look at the supply chain, weekly
donislawdev Aug 19, 2026
e2291da
feat(ci): block a dependency nobody can license, and pin the driver's…
donislawdev Aug 19, 2026
adbf02e
ci: run the mutation registry instead of quoting it
donislawdev Aug 19, 2026
40d810e
chore: put a ceiling on branching, and stop the CI prose drifting
donislawdev Aug 19, 2026
cf818aa
chore(types): make three modules strict, and make the list one-way
donislawdev Aug 19, 2026
f9fbd61
ci: gate the coverage of what a pull request changes
donislawdev Aug 19, 2026
4b80d7e
fix(ci): skip actions in the licence gate, and stop asking git for de…
donislawdev Aug 19, 2026
0d001ce
fix(tests): give the scanner guard something to guard on a runner
donislawdev Aug 19, 2026
eb1d5ed
fix(ci): stop a dead package mirror eating the whole test job
donislawdev Aug 19, 2026
50ae9eb
fix(ci): rewrite the mirror where it actually lives, and skip apt whe…
donislawdev Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,24 @@ version: 2
# security fixes). Grouped so one PR carries the whole bump, not four.
# - pip: requirements*.txt (pydivert, psutil, pytest, hypothesis, ...). Review these
# before merging - pydivert/psutil touch the packet path and the build.
# `cooldown` holds a newly published version back for a week before it is
# proposed here. It closes the window the tj-actions and trivy-action
# compromises used - a poisoned release pulled in within hours of publication -
# and it costs a week of latency on updates nobody is waiting for.
# 🔴 It does NOT delay security fixes: cooldown applies to version updates only,
# and Dependabot security updates are a separate mechanism (GitHub docs,
# "Dependabot options reference"). Checked there rather than assumed, because
# a week of silence on a CVE would be the opposite of the point.
# github-actions supports `default-days` alone; the `semver-*-days` variants
# exist for pip, and are left off until there is a reason to treat a major
# differently from a patch here.
updates:
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
github-actions:
patterns:
Expand All @@ -19,4 +32,6 @@ updates:
directory: "/"
schedule:
interval: weekly
cooldown:
default-days: 7
open-pull-requests-limit: 5
312 changes: 297 additions & 15 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

24 changes: 22 additions & 2 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,31 @@ jobs:
name: review new dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/dependency-review-action@v5
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
# A tool that loads a kernel driver has no business gaining a
# vulnerable dependency quietly. Anything at or above "moderate"
# fails the pull request rather than warning in a log nobody reads.
fail-on-severity: moderate
comment-summary-in-pr: always

# The half the action above cannot do. Its own documentation: "If we can't
# detect the license for a dependency we will inform you, but the action
# won't fail." For a GPL-3.0 project that ships a binary, an unidentified
# licence is the one answer nobody can act on - so the same data is read
# again here, from the API the action itself uses, and an unknown licence
# blocks exactly like a denied one.
#
# `gh` is preinstalled and uses the job token. The gate exits 2 when it
# cannot read the answer (the endpoint refuses some repository shapes), so
# a 403 fails the run instead of looking like an empty list of problems.
- name: Licences of what this pull request adds
env:
GH_TOKEN: ${{ github.token }}
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
REPO: ${{ github.repository }}
run: |
gh api "repos/$REPO/dependency-graph/compare/$BASE...$HEAD" > deps.json
python tools/dependency_gate.py deps.json
11 changes: 6 additions & 5 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ jobs:
contents: read
pages: read
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"
cache: pip
Expand All @@ -71,7 +71,7 @@ jobs:
# the run instead of publishing a site whose every canonical points elsewhere.
- name: Read the Pages configuration
id: pages
uses: actions/configure-pages@v6
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0

- name: The address it publishes to must be the address the pages claim
env:
Expand Down Expand Up @@ -102,6 +102,7 @@ jobs:
- name: Install the test dependencies
run: |
python -m pip install --upgrade pip
pip install --require-hashes -r requirements.txt
pip install -r requirements-dev.txt

# The generator refuses to build a broken page, but "it did not crash" is a
Expand All @@ -113,7 +114,7 @@ jobs:
- name: Build
run: python tools/build_site.py --out _site

- uses: actions/upload-pages-artifact@v5
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: _site

Expand All @@ -131,7 +132,7 @@ jobs:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/deploy-pages@v5
- uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
id: deployment

# A deployment that reports success proves the upload happened, not that the
Expand Down
32 changes: 29 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,23 @@ jobs:
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# The interpreter PyInstaller freezes into the shipped bundle. Keep it in
# step with the build job in ci.yml, or CI smoke-tests one artefact and
# users download another.
- uses: actions/setup-python@v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"
cache: pip

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements-build.txt
# One hash-checked resolution over both files - the same command the
# build job in ci.yml runs, so a release cannot be built from a
# different set of bytes than the one CI proved.
pip install --require-hashes -r requirements.txt -r requirements-build.txt

# The version is single-sourced in VERSION.txt. The tag is v<version> for a
# final release, or v<version>-rc.N (also -beta.N / -alpha.N) for a pre-release.
Expand Down Expand Up @@ -161,6 +164,29 @@ jobs:
subject-path: ${{ env.ASSET }}
sbom-path: ${{ env.SBOM }}

# WHO built it, and FROM WHAT. The SBOM attestation above says what is inside
# the zip; this one says the zip came out of this repository, from this commit,
# through this workflow, on a GitHub-hosted runner - signed with the same
# short-lived OIDC identity and stored in the same attestation store. A user
# checks both with one command:
#
# gh attestation verify BeanNetworkTester-<tag>-windows-x64.zip \
# -R donislawdev/BeanNetworkTester
#
# It is a different question from a code signature, which is why both are
# worth having: a signature says who signed the binary, provenance says which
# source and which build produced it. A stolen signing key cannot forge this,
# and a forked workflow cannot claim to be this repository.
#
# Separate action from `actions/attest` above on purpose: that one attaches a
# predicate you hand it (here, the SBOM), while the provenance predicate is
# built BY the action from the workflow's own context - there is nothing to
# pass in, and nothing we could pass in that would be trustworthy.
- name: Attest the build provenance of the release archive
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: ${{ env.ASSET }}

# gh is preinstalled on the runner - no third-party action, uses the job token.
# A -rc/-beta/-alpha tag publishes as a "Pre-release"; a plain tag as "Latest".
#
Expand Down
75 changes: 75 additions & 0 deletions .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# OpenSSF Scorecard: an OUTSIDE opinion on this repository's supply chain.
#
# Why it is worth a workflow when CI already runs three static analysers: those
# read the code, and this reads the PROJECT - whether actions are pinned, whether
# tokens are least-privilege, whether releases are signed, whether a branch is
# protected, whether dangerous workflow patterns are present. It is the only check
# here that can tell us something we did not already think to test, and it says it
# in a form a stranger can read before installing a tool that loads a kernel
# driver.
#
# Nothing here can fail a pull request. It reports.
name: Scorecard

on:
# The default branch only. Scorecard grades a repository, not a change, and its
# own documentation marks the pull_request trigger experimental.
push:
branches: [master]
schedule:
# Monday 07:00 UTC - an hour after the CI cron, so the two do not queue behind
# each other on the same runner pool.
- cron: "0 7 * * 1"
# Branch protection is one of the things it grades, so a change to it should be
# re-graded rather than waiting a week.
branch_protection_rule:
workflow_dispatch:

# Read-all at the top, and the job below raises exactly what it needs. Same rule
# as ci.yml: a permission is granted on the job that uses it, never globally.
permissions: read-all

jobs:
analysis:
name: OpenSSF Scorecard
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
# Publishes the SARIF into the Security tab, next to CodeQL's own findings.
security-events: write
# Mints the OIDC token that lets the OpenSSF API verify the results really
# came from this repository. This is what makes the badge mean anything -
# without it anyone could publish a score for any repository.
id-token: write
# Scorecard reads workflow run data to grade CI practices.
actions: read
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The analyser must never be handed a token it could use: it walks
# repository data, and it has no business being able to push.
persist-credentials: false

- name: Run the analysis
uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
with:
results_file: results.sarif
results_format: sarif
# Publishing is what turns the score into a badge, and it is public
# information about a public repository either way.
publish_results: true

# Kept as an artefact as well as uploaded: the Security tab shows the
# findings, the file shows the whole grading, including the checks that
# passed.
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: scorecard-results
path: results.sarif
retention-days: 5

- name: Upload to the Security tab
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
sarif_file: results.sarif
11 changes: 9 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ testable on any OS.
## Getting started

```bash
pip install --require-hashes -r requirements.txt # runtime, pinned to exact artefacts
pip install -r requirements-dev.txt
pip install -r requirements-lint.txt # ruff and mypy (semgrep on Linux and macOS)
python -m pytest tests # full suite - no Windows, no driver, no admin rights
python smoke_gui.py # GUI smoke with a fake tkinter
ruff check # F and B fail a pull request, S and ASYNC are a report
mypy # types, over the package
python bean_network_tester.py --simulate --loss 10 --duration 3 # CLI demo
python bean_network_tester.py --doctor # environment report
```
Expand Down Expand Up @@ -57,5 +61,8 @@ onedir, `asInvoker`. Do not reintroduce `--noconsole` / `--onefile` / `--uac-adm
## Pull requests

1. Run `python -m pytest tests` - everything must pass.
2. Add tests for new behavior (see `tests/` for the style).
3. Update both `lang/en.json` and `lang/pl.json` when adding UI texts.
2. Run `ruff check` and `mypy` - both are gates on the pull request.
Semgrep runs in CI and needs no local setup. On Windows it installs but does not
scan, so run it from WSL if you want it locally.
3. Add tests for new behavior (see `tests/` for the style).
4. Update both `lang/en.json` and `lang/pl.json` when adding UI texts.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[![Latest release](https://img.shields.io/github/v/release/donislawdev/BeanNetworkTester?sort=semver)](https://github.com/donislawdev/BeanNetworkTester/releases/latest)
[![Downloads](https://img.shields.io/github/downloads/donislawdev/BeanNetworkTester/total)](https://github.com/donislawdev/BeanNetworkTester/releases)
[![License: GPLv3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/donislawdev/BeanNetworkTester/badge)](https://scorecard.dev/viewer/?uri=github.com/donislawdev/BeanNetworkTester)
![Platform: Windows](https://img.shields.io/badge/platform-Windows-0078D6)

**Bean Network Tester** is a tool for testers and developers: check how your application behaves
Expand Down Expand Up @@ -1130,6 +1131,7 @@ The engine is separate from WinDivert, so the tests run on any system (they need
admin nor tkinter). The suite is based on **pytest**:

```bat
pip install --require-hashes -r requirements.txt
pip install -r requirements-dev.txt
python -m pytest tests
```
Expand Down Expand Up @@ -1157,6 +1159,29 @@ exit-code assertions, an NDJSON check, `--doctor` and `--license`, and then **an
a smoke test of the built file** (`--version`, `--simulate`, a bad config -> code 3) plus a check
that the WinDivert driver really shipped next to the exe, with a downloadable artifact.

<!-- ci-jobs:start -->
Every job in that workflow, and what a red one means:

| job | what it does |
|---|---|
| `public-text` | commit messages and the pull-request description: English, plain hyphens, nothing private to a machine |
| `lint` | **ruff**. Dead code, bug shapes and the complexity ceiling fail the run. The security family is reported and never blocks |
| `types` | **mypy** over the package |
| `semgrep` | the default registry ruleset. ERROR, HIGH and CRITICAL fail the run, the rest is printed |
| `mutations` | breaks each guarded behaviour and proves its test reddens. A pull request runs the entries it touched, the weekly run does all of them |
| `audit` | weekly only: **pip-audit** against the pinned set, and it opens an issue when an advisory lands |
| `tests` | the suite, the GUI smoke, the real-Tk render check and the CLI assertions, on Linux and Windows |
| `build` | the Windows executable, smoke-tested, with the driver check and the licence registry scan |
<!-- ci-jobs:end -->

**Three static checks run beside the tests**, on Linux only, because they read the source rather
than run it. **ruff** fails a pull request on a dead-code or bug-shape finding (`F` and `B`) and
reports the security family (`S`, `ASYNC`) as annotations that never block. **mypy** type-checks
the package. **semgrep** scans with its default registry ruleset and a finding at ERROR, HIGH or
CRITICAL fails the run, while everything below that is printed in full. The three tool versions
are pinned in `requirements-lint.txt`, so a new release of a linter cannot redden a pull request
that changed nothing.

One step is worth knowing about because no unit test can do its job: a **GUI render check on real
Tk** under a virtual screen, at the minimum supported 1366x768, **in every language**. It builds
the actual window, walks every page, opens the About window and fails the build when any button is
Expand Down Expand Up @@ -1446,6 +1471,18 @@ and some antivirus tools may raise a false alarm. The **WinDivert driver itself
by its author**. You can compare the release's SHA-256 checksum (`SHA256SUMS.txt`) to confirm the
file has not been modified.

**You can also check where the download came from, not just that it is unchanged.** Every release
archive carries a signed build attestation, so one command answers "was this really built from that
source by that workflow":

```bash
gh attestation verify BeanNetworkTester-v0.5.0-windows-x64.zip -R donislawdev/BeanNetworkTester
```

A checksum proves the file matches what the release page says. This proves the release page itself
was produced by this repository's own workflow, from a specific commit, on a GitHub-hosted runner.
The same command also verifies the SBOM that ships beside the archive.

### What is inside the download, and how to check it

Every release carries an **SBOM** - a list, in the standard SPDX format, of every third-party
Expand Down
Loading
Loading