From 6f3d76d9bd5e6b30b5a8eb29c437288072ef972b Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Wed, 12 Aug 2026 20:45:48 +0200 Subject: [PATCH 1/3] ci(release): move the attestation step to actions/attest The v0.5.0-rc.1 run - the first time this step ever executed - logged that actions/attest-sbom is deprecated. Its README says the same: deprecated in favour of actions/attest, kept working as a wrapper over it, and "all of the existing action inputs are compatible". Verified against actions/attest's own action.yml rather than taken on trust: sbom-path is there with the same meaning, and providing it is what makes this an SBOM attestation rather than build provenance. Both inputs stay as they were, so this is a change of name and pinned SHA. artifact-metadata: write is deliberately NOT added, and the comment now says why: it is needed for the artifact storage record, which requires push-to-registry, which defaults to false and is unused here. Granting a permission we do not need would contradict the line above it. What a tag will have to confirm: that the emitted predicate type stays https://spdx.dev/Document/v2.3. The step runs before the release is created, so a failure publishes nothing. Co-Authored-By: Claude Opus 5 --- .github/workflows/release.yml | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3218fb8..0f40c7b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,9 +14,16 @@ permissions: contents: write # For the SBOM attestation. `id-token` mints the short-lived OIDC token that # signs it, `attestations` writes the result to the repository's attestation - # store. Both are required by actions/attest-sbom and neither grants anything - # else; a release that skipped them would still publish an SBOM, just an - # unsigned one that anybody could swap. + # store. Both are required by actions/attest and neither grants anything else; + # a release that skipped them would still publish an SBOM, just an unsigned one + # that anybody could swap. + # + # NOT here on purpose: `artifact-metadata: write`, which that action's README + # lists as a third permission. It is needed to create the artifact STORAGE + # RECORD, and `create-storage-record` requires `push-to-registry`, which + # defaults to false and is not used here (read in the action's own action.yml, + # 2026-08-12). Granting a permission we do not need would contradict the line + # above it. id-token: write attestations: write @@ -139,10 +146,17 @@ jobs: # anybody could replace, which is the difference between a bill of materials # and a note claiming to be one. - name: Attest the SBOM against the release archive + # Was actions/attest-sbom until 2026-08-12. Its own README now says it is + # deprecated in favour of actions/attest and runs as a wrapper over it, and + # that "all of the existing action inputs are compatible" - checked against + # actions/attest's action.yml rather than taken on trust: `sbom-path` is + # there, with the same meaning, and providing it is what makes this an SBOM + # attestation rather than build provenance. So both inputs stay as they are. + # # Pinned by SHA, not by tag. This is the first third-party-shaped action in # the workflow that publishes the release, and a tag can be moved; the SHA - # below is what `v4.1.0` pointed at on 2026-08-11, read from the API. - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 + # below is what `v4.2.2` pointed at on 2026-08-12, read from the API. + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-path: ${{ env.ASSET }} sbom-path: ${{ env.SBOM }} From 59961e24548aefe5d5943c1a3c8fab8fd13c1d88 Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Wed, 12 Aug 2026 21:16:52 +0200 Subject: [PATCH 2/3] feat(sbom): name the PyInstaller that froze the build The row said NOASSERTION, and that was CORRECT rather than broken: the entry carries no module, component_rows() falls back to "bundled", and the generator deliberately maps "bundled" to empty rather than pretending it is a version. Nothing was asking. So the SBOM could not name a component that ships a piece of itself - the bootloader - inside the executable, under its own licence. It is read through importlib.metadata rather than by importing the package: the answer is identical, and asking would otherwise drag a build tool into --license on any machine that has it. What makes the answer honest is WHERE it is asked - tools/sbom.py runs in the release job minutes after PyInstaller froze the archive, so the installed distribution is the one that made it. Inside the shipped executable there is nothing to ask, and the fallback stays "bundled" rather than "-": the bootloader really is in there, and "-" is this module's word for "not present here". So the binary's own --license output does not change, and the SBOM keeps saying NOASSERTION there. Measured on the real bundle before choosing this scope: VCRUNTIME140.dll does carry a file version and could be read the same way, but that needs ctypes prototypes, a native-prototype guard and a non-Windows fallback. libffi-8.dll carries no version resource at all, so that row stays NOASSERTION honestly. This closes half of the PyInstaller item in the backlog: what built the release is now reported. Pinning which version builds it is untouched and stays a separate decision. Guards: two in tests/test_sbom.py, both proved by mutation and registered - one for the version being reported, one for absence not becoming a guess. Co-Authored-By: Claude Opus 5 --- - | 175 ++++++++++++++++++++++++++++++++ CHANGELOG.md | 5 + beantester/legal.py | 28 +++++ tests/test_mutation_registry.py | 19 ++++ tests/test_sbom.py | 52 ++++++++++ 5 files changed, 279 insertions(+) create mode 100644 - diff --git a/- b/- new file mode 100644 index 0000000..0d11069 --- /dev/null +++ b/- @@ -0,0 +1,175 @@ +{ + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "Bean Network Tester-0.5.0", + "documentNamespace": "https://github.com/donislawdev/BeanNetworkTester/spdx/0.5.0-e2846fc8fa4d291b", + "creationInfo": { + "created": "2026-08-12T18:54:28Z", + "creators": [ + "Tool: beantester-sbom", + "Organization: DonislawDev" + ], + "comment": "Generated from beantester.legal.COMPONENTS, the reviewed list of what this build ships. See tools/sbom.py for why that is the source rather than a filesystem scan." + }, + "packages": [ + { + "SPDXID": "SPDXRef-Package-Bean-Network-Tester", + "name": "Bean Network Tester", + "versionInfo": "0.5.0", + "downloadLocation": "https://github.com/donislawdev/BeanNetworkTester", + "filesAnalyzed": false, + "licenseConcluded": "GPL-3.0-only", + "licenseDeclared": "GPL-3.0-only", + "copyrightText": "NOASSERTION" + }, + { + "SPDXID": "SPDXRef-Package-WinDivert", + "name": "WinDivert", + "versionInfo": "2.2", + "downloadLocation": "https://github.com/basil00/WinDivert", + "filesAnalyzed": false, + "licenseConcluded": "LGPL-3.0-only OR GPL-2.0-only", + "licenseDeclared": "LGPL-3.0-only OR GPL-2.0-only", + "copyrightText": "NOASSERTION" + }, + { + "SPDXID": "SPDXRef-Package-PyDivert", + "name": "PyDivert", + "versionInfo": "3.1.3", + "downloadLocation": "https://github.com/ffalcinelli/pydivert", + "filesAnalyzed": false, + "licenseConcluded": "LGPL-3.0-or-later OR GPL-2.0-or-later", + "licenseDeclared": "LGPL-3.0-or-later OR GPL-2.0-or-later", + "copyrightText": "NOASSERTION" + }, + { + "SPDXID": "SPDXRef-Package-psutil", + "name": "psutil", + "versionInfo": "7.2.2", + "downloadLocation": "https://github.com/giampaolo/psutil", + "filesAnalyzed": false, + "licenseConcluded": "BSD-3-Clause", + "licenseDeclared": "BSD-3-Clause", + "copyrightText": "NOASSERTION" + }, + { + "SPDXID": "SPDXRef-Package-Python", + "name": "Python", + "versionInfo": "3.14.7", + "downloadLocation": "https://www.python.org/downloads/source/", + "filesAnalyzed": false, + "licenseConcluded": "PSF-2.0", + "licenseDeclared": "PSF-2.0", + "copyrightText": "NOASSERTION" + }, + { + "SPDXID": "SPDXRef-Package-Tcl-Tk", + "name": "Tcl/Tk", + "versionInfo": "9.0", + "downloadLocation": "https://www.tcl-lang.org/software/tcltk/", + "filesAnalyzed": false, + "licenseConcluded": "TCL", + "licenseDeclared": "TCL", + "copyrightText": "NOASSERTION" + }, + { + "SPDXID": "SPDXRef-Package-PyInstaller--bootloader", + "name": "PyInstaller (bootloader)", + "versionInfo": "6.21.0", + "downloadLocation": "https://github.com/pyinstaller/pyinstaller", + "filesAnalyzed": false, + "licenseConcluded": "GPL-2.0-or-later WITH Bootloader-exception", + "licenseDeclared": "GPL-2.0-or-later WITH Bootloader-exception", + "copyrightText": "NOASSERTION" + }, + { + "SPDXID": "SPDXRef-Package-zlib", + "name": "zlib", + "versionInfo": "1.3.1.zlib-ng", + "downloadLocation": "https://www.zlib.net/", + "filesAnalyzed": false, + "licenseConcluded": "Zlib", + "licenseDeclared": "Zlib", + "copyrightText": "NOASSERTION" + }, + { + "SPDXID": "SPDXRef-Package-libffi", + "name": "libffi", + "versionInfo": "NOASSERTION", + "downloadLocation": "https://github.com/libffi/libffi", + "filesAnalyzed": false, + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", + "copyrightText": "NOASSERTION" + }, + { + "SPDXID": "SPDXRef-Package-Microsoft-C-Runtime", + "name": "Microsoft C Runtime", + "versionInfo": "NOASSERTION", + "downloadLocation": "https://learn.microsoft.com/cpp/windows/redistributing-visual-cpp-files", + "filesAnalyzed": false, + "licenseConcluded": "LicenseRef-Microsoft-Redistributable", + "licenseDeclared": "LicenseRef-Microsoft-Redistributable", + "copyrightText": "NOASSERTION" + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relationshipType": "DESCRIBES", + "relatedSpdxElement": "SPDXRef-Package-Bean-Network-Tester" + }, + { + "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package-WinDivert" + }, + { + "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package-PyDivert" + }, + { + "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package-psutil" + }, + { + "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package-Python" + }, + { + "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package-Tcl-Tk" + }, + { + "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package-PyInstaller--bootloader" + }, + { + "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package-zlib" + }, + { + "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package-libffi" + }, + { + "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Package-Microsoft-C-Runtime" + } + ], + "hasExtractedLicensingInfo": [ + { + "licenseId": "LicenseRef-Microsoft-Redistributable", + "extractedText": "Microsoft Visual C++ runtime and Universal CRT files, redistributed under Microsoft's redistributable-code terms for Visual Studio. Microsoft publishes the terms rather than a licence text to bundle: https://learn.microsoft.com/cpp/windows/redistributing-visual-cpp-files" + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index b70af85..d23e9d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,11 @@ either yet. administrator uses that administrator's folder instead of yours. Nothing used to say so, which made saved profiles look lost. Both READMEs explain it, and an administrator can set `BEAN_DATA_DIR` system-wide to give every account one shared folder. +- **The SBOM published with each release now names the version of the tool that built it.** That + component ships a piece of itself inside the executable, under its own licence, and the file + could not say which version you were given. The two entries that still say "no assertion" say + it because the files they describe carry no version at all, which is the honest answer rather + than a guess. ### Docs - **Four more guides on the website: no internet, timed scenarios, game lag and chaos testing.** diff --git a/beantester/legal.py b/beantester/legal.py index 5215e49..4df6d9e 100644 --- a/beantester/legal.py +++ b/beantester/legal.py @@ -112,6 +112,32 @@ def _zlib_version(): return "-" +def _pyinstaller_version(): + """The PyInstaller that froze this bundle, when anything here can still say. + + Read from the installed distribution's METADATA rather than by importing the + package: the answer is the same, and importing PyInstaller to ask its version + would drag a large build-time tool into `--license` on any machine that has + it. `importlib.metadata` does not import the package at all. + + Two environments, two honest answers. Where the release is BUILT, PyInstaller + is installed, and `tools/sbom.py` runs in the same job minutes after the build + - so the version it reports is the one that actually froze the archive. + + Inside the shipped executable the distribution is absent (a build tool is not + bundled with what it builds) and the answer falls back to "bundled", which is + what this row said before and is still true: the BOOTLOADER is in there, we + just cannot name its version from inside. Not "-", which is this module's word + for "not present here" and would read as though the component were absent. + `tools/sbom.py` maps "bundled" to NOASSERTION, so the SBOM is unchanged too. + """ + try: + import importlib.metadata + return str(importlib.metadata.version("pyinstaller")) + except Exception: # noqa: BLE001 - absence is an answer, not a failure + return "bundled" + + def component_rows(): """``(name, version, licence, source_url)`` for every third-party component.""" rows = [] @@ -126,6 +152,8 @@ def component_rows(): version = _zlib_version() elif name == "WinDivert": version = WINDIVERT_VERSION + elif name.startswith("PyInstaller"): + version = _pyinstaller_version() else: version = "bundled" # libffi and the MS runtime carry no version rows.append((name, version, licence, url)) diff --git a/tests/test_mutation_registry.py b/tests/test_mutation_registry.py index 4f632fb..f13c5f7 100644 --- a/tests/test_mutation_registry.py +++ b/tests/test_mutation_registry.py @@ -71,6 +71,25 @@ "new": ' return bool(key == "duration" and getattr(self.app, "running", False))', "test": "test_start_only_fields_are_locked_while_a_session_runs", }, + { + # The SBOM could not name the tool that froze the binary, whose bootloader + # ships inside it under its own licence. + "label": "sbom: the registry stops asking for the PyInstaller version", + "file": "beantester/legal.py", + "old": ' elif name.startswith("PyInstaller"):\n' + " version = _pyinstaller_version()\n", + "new": "", + "test": "test_the_sbom_names_the_pyinstaller_that_froze_the_build", + }, + { + # The other direction, which matters more: inside the shipped exe there is + # no PyInstaller to ask, and the answer there must stay "no assertion". + "label": "sbom: an absent build tool gets an invented version", + "file": "beantester/legal.py", + "old": ' return "bundled"\n\n\ndef component_rows():', + "new": ' return "0.0.0"\n\n\ndef component_rows():', + "test": "test_a_build_tool_that_is_not_installed_is_not_invented", + }, { # The one defect in this work that CI found and this machine could not: # relpath raises across drives, and the Windows runner keeps the repo and diff --git a/tests/test_sbom.py b/tests/test_sbom.py index 685f01d..65370f6 100644 --- a/tests/test_sbom.py +++ b/tests/test_sbom.py @@ -138,3 +138,55 @@ def test_the_bundle_guard_notices_a_component_the_registry_lacks(tmp_path): found = sbom.audit_bundle(str(dirty)) check("an unknown component is reported", "libcurl" in found and "OpenSSL" in found, f"({found})") + + +def test_the_sbom_names_the_pyinstaller_that_froze_the_build(monkeypatch): + """The version of a build tool is knowable exactly where the build happens. + + `tools/sbom.py` runs in the release job minutes after PyInstaller froze the + archive, so the installed distribution IS the one that made it. Until + 2026-08-12 this row said NOASSERTION, which was honest - nothing asked - and + left the SBOM unable to name a component whose bootloader ships inside the + binary under its own licence. + + Read through `importlib.metadata`, so asking the question never imports the + build tool into `--license` on a developer's machine. + """ + import importlib.metadata + + monkeypatch.setattr(importlib.metadata, "version", lambda name: "9.9.9") + rows = {name: version for name, version, _lic, _url in legal.component_rows()} + named = [n for n in rows if n.startswith("PyInstaller")] + check("the registry still has exactly one PyInstaller row", len(named) == 1, f"({named})") + check("it reports the installed version", rows[named[0]] == "9.9.9", f"({rows[named[0]]})") + + packages = {p["name"]: p["versionInfo"] for p in sbom.build()["packages"]} + check("and the SBOM carries it", packages[named[0]] == "9.9.9", f"({packages[named[0]]})") + + +def test_a_build_tool_that_is_not_installed_is_not_invented(monkeypatch): + """Inside the shipped executable there is no PyInstaller to ask. + + A build tool is not bundled with what it builds, so the SBOM's honest answer + there is "no assertion" - the same answer this row gave before it could be + resolved at all. + + The report the user of the binary reads keeps saying "bundled", which is the + other half of the truth and easy to lose: the BOOTLOADER really is inside the + executable, so "-" (this module's word for "not present here") would read as + though the component were missing. + """ + import importlib.metadata + + def missing(name): + raise importlib.metadata.PackageNotFoundError(name) + + monkeypatch.setattr(importlib.metadata, "version", missing) + rows = {name: version for name, version, _lic, _url in legal.component_rows()} + named = [n for n in rows if n.startswith("PyInstaller")][0] + check("the report still says the component is in there", + rows[named] == "bundled", f"({rows[named]})") + + packages = {p["name"]: p["versionInfo"] for p in sbom.build()["packages"]} + check("and the SBOM says NOASSERTION, not a made-up version", + packages[named] == "NOASSERTION", f"({packages[named]})") From 7ecc9193aca51c9486ba89e9876774de06efb203 Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Wed, 12 Aug 2026 21:17:49 +0200 Subject: [PATCH 3/3] chore: remove a stray file a redirected SBOM run left behind `tools/sbom.py -o -` does not mean stdout - it wrote a file literally named `-` in the project root, and `git add -A` swept it into the previous commit. Co-Authored-By: Claude Opus 5 --- - | 175 -------------------------------------------------------------- 1 file changed, 175 deletions(-) delete mode 100644 - diff --git a/- b/- deleted file mode 100644 index 0d11069..0000000 --- a/- +++ /dev/null @@ -1,175 +0,0 @@ -{ - "spdxVersion": "SPDX-2.3", - "dataLicense": "CC0-1.0", - "SPDXID": "SPDXRef-DOCUMENT", - "name": "Bean Network Tester-0.5.0", - "documentNamespace": "https://github.com/donislawdev/BeanNetworkTester/spdx/0.5.0-e2846fc8fa4d291b", - "creationInfo": { - "created": "2026-08-12T18:54:28Z", - "creators": [ - "Tool: beantester-sbom", - "Organization: DonislawDev" - ], - "comment": "Generated from beantester.legal.COMPONENTS, the reviewed list of what this build ships. See tools/sbom.py for why that is the source rather than a filesystem scan." - }, - "packages": [ - { - "SPDXID": "SPDXRef-Package-Bean-Network-Tester", - "name": "Bean Network Tester", - "versionInfo": "0.5.0", - "downloadLocation": "https://github.com/donislawdev/BeanNetworkTester", - "filesAnalyzed": false, - "licenseConcluded": "GPL-3.0-only", - "licenseDeclared": "GPL-3.0-only", - "copyrightText": "NOASSERTION" - }, - { - "SPDXID": "SPDXRef-Package-WinDivert", - "name": "WinDivert", - "versionInfo": "2.2", - "downloadLocation": "https://github.com/basil00/WinDivert", - "filesAnalyzed": false, - "licenseConcluded": "LGPL-3.0-only OR GPL-2.0-only", - "licenseDeclared": "LGPL-3.0-only OR GPL-2.0-only", - "copyrightText": "NOASSERTION" - }, - { - "SPDXID": "SPDXRef-Package-PyDivert", - "name": "PyDivert", - "versionInfo": "3.1.3", - "downloadLocation": "https://github.com/ffalcinelli/pydivert", - "filesAnalyzed": false, - "licenseConcluded": "LGPL-3.0-or-later OR GPL-2.0-or-later", - "licenseDeclared": "LGPL-3.0-or-later OR GPL-2.0-or-later", - "copyrightText": "NOASSERTION" - }, - { - "SPDXID": "SPDXRef-Package-psutil", - "name": "psutil", - "versionInfo": "7.2.2", - "downloadLocation": "https://github.com/giampaolo/psutil", - "filesAnalyzed": false, - "licenseConcluded": "BSD-3-Clause", - "licenseDeclared": "BSD-3-Clause", - "copyrightText": "NOASSERTION" - }, - { - "SPDXID": "SPDXRef-Package-Python", - "name": "Python", - "versionInfo": "3.14.7", - "downloadLocation": "https://www.python.org/downloads/source/", - "filesAnalyzed": false, - "licenseConcluded": "PSF-2.0", - "licenseDeclared": "PSF-2.0", - "copyrightText": "NOASSERTION" - }, - { - "SPDXID": "SPDXRef-Package-Tcl-Tk", - "name": "Tcl/Tk", - "versionInfo": "9.0", - "downloadLocation": "https://www.tcl-lang.org/software/tcltk/", - "filesAnalyzed": false, - "licenseConcluded": "TCL", - "licenseDeclared": "TCL", - "copyrightText": "NOASSERTION" - }, - { - "SPDXID": "SPDXRef-Package-PyInstaller--bootloader", - "name": "PyInstaller (bootloader)", - "versionInfo": "6.21.0", - "downloadLocation": "https://github.com/pyinstaller/pyinstaller", - "filesAnalyzed": false, - "licenseConcluded": "GPL-2.0-or-later WITH Bootloader-exception", - "licenseDeclared": "GPL-2.0-or-later WITH Bootloader-exception", - "copyrightText": "NOASSERTION" - }, - { - "SPDXID": "SPDXRef-Package-zlib", - "name": "zlib", - "versionInfo": "1.3.1.zlib-ng", - "downloadLocation": "https://www.zlib.net/", - "filesAnalyzed": false, - "licenseConcluded": "Zlib", - "licenseDeclared": "Zlib", - "copyrightText": "NOASSERTION" - }, - { - "SPDXID": "SPDXRef-Package-libffi", - "name": "libffi", - "versionInfo": "NOASSERTION", - "downloadLocation": "https://github.com/libffi/libffi", - "filesAnalyzed": false, - "licenseConcluded": "MIT", - "licenseDeclared": "MIT", - "copyrightText": "NOASSERTION" - }, - { - "SPDXID": "SPDXRef-Package-Microsoft-C-Runtime", - "name": "Microsoft C Runtime", - "versionInfo": "NOASSERTION", - "downloadLocation": "https://learn.microsoft.com/cpp/windows/redistributing-visual-cpp-files", - "filesAnalyzed": false, - "licenseConcluded": "LicenseRef-Microsoft-Redistributable", - "licenseDeclared": "LicenseRef-Microsoft-Redistributable", - "copyrightText": "NOASSERTION" - } - ], - "relationships": [ - { - "spdxElementId": "SPDXRef-DOCUMENT", - "relationshipType": "DESCRIBES", - "relatedSpdxElement": "SPDXRef-Package-Bean-Network-Tester" - }, - { - "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", - "relationshipType": "CONTAINS", - "relatedSpdxElement": "SPDXRef-Package-WinDivert" - }, - { - "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", - "relationshipType": "CONTAINS", - "relatedSpdxElement": "SPDXRef-Package-PyDivert" - }, - { - "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", - "relationshipType": "CONTAINS", - "relatedSpdxElement": "SPDXRef-Package-psutil" - }, - { - "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", - "relationshipType": "CONTAINS", - "relatedSpdxElement": "SPDXRef-Package-Python" - }, - { - "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", - "relationshipType": "CONTAINS", - "relatedSpdxElement": "SPDXRef-Package-Tcl-Tk" - }, - { - "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", - "relationshipType": "CONTAINS", - "relatedSpdxElement": "SPDXRef-Package-PyInstaller--bootloader" - }, - { - "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", - "relationshipType": "CONTAINS", - "relatedSpdxElement": "SPDXRef-Package-zlib" - }, - { - "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", - "relationshipType": "CONTAINS", - "relatedSpdxElement": "SPDXRef-Package-libffi" - }, - { - "spdxElementId": "SPDXRef-Package-Bean-Network-Tester", - "relationshipType": "CONTAINS", - "relatedSpdxElement": "SPDXRef-Package-Microsoft-C-Runtime" - } - ], - "hasExtractedLicensingInfo": [ - { - "licenseId": "LicenseRef-Microsoft-Redistributable", - "extractedText": "Microsoft Visual C++ runtime and Universal CRT files, redistributed under Microsoft's redistributable-code terms for Visual Studio. Microsoft publishes the terms rather than a licence text to bundle: https://learn.microsoft.com/cpp/windows/redistributing-visual-cpp-files" - } - ] -}