software-intake-gatekeeper 0.2.2__tar.gz → 0.3.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.
Files changed (68) hide show
  1. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/.gitignore +1 -0
  2. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/CHANGELOG.md +23 -0
  3. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/PKG-INFO +51 -27
  4. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/README.md +47 -24
  5. software_intake_gatekeeper-0.3.0/examples/rust-demo/Cargo.lock +16 -0
  6. software_intake_gatekeeper-0.3.0/examples/rust-demo/Cargo.toml +10 -0
  7. software_intake_gatekeeper-0.3.0/examples/rust-demo/src/main.rs +7 -0
  8. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/pyproject.toml +4 -3
  9. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/src/intake_gatekeeper/__init__.py +1 -1
  10. software_intake_gatekeeper-0.3.0/src/intake_gatekeeper/authenticode.py +149 -0
  11. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/src/intake_gatekeeper/diff.py +34 -0
  12. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/src/intake_gatekeeper/engine.py +7 -1
  13. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/src/intake_gatekeeper/model.py +13 -0
  14. software_intake_gatekeeper-0.3.0/src/intake_gatekeeper/objfile.py +170 -0
  15. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/src/intake_gatekeeper/output.py +49 -1
  16. software_intake_gatekeeper-0.3.0/src/intake_gatekeeper/packages.py +139 -0
  17. software_intake_gatekeeper-0.3.0/src/intake_gatekeeper/rust.py +92 -0
  18. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/src/intake_gatekeeper/scanner.py +102 -2
  19. software_intake_gatekeeper-0.3.0/tests/binfmt.py +98 -0
  20. software_intake_gatekeeper-0.3.0/tests/test_ecosystems.py +303 -0
  21. software_intake_gatekeeper-0.3.0/tests/test_real_artifacts.py +83 -0
  22. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/LICENSE +0 -0
  23. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/SECURITY.md +0 -0
  24. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/action-requirements.txt +0 -0
  25. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/action.yml +0 -0
  26. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/demo-app-src/dist/settings.min.js +0 -0
  27. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/demo-app-src/main.js +0 -0
  28. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/demo-app-src/node_modules/@demo/ui-kit/package.json +0 -0
  29. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/demo-app-src/node_modules/lodash/index.js +0 -0
  30. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/demo-app-src/node_modules/lodash/package.json +0 -0
  31. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/demo-app-src/node_modules/minimist/package.json +0 -0
  32. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/demo-app-src/package.json +0 -0
  33. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/demo-app-src/preload.js +0 -0
  34. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/go-demo/go.mod +0 -0
  35. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/go-demo/go.sum +0 -0
  36. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/go-demo/main.go +0 -0
  37. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/intake-policy.toml +0 -0
  38. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/update-demo/v1/resources/app/main.js +0 -0
  39. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/update-demo/v1/resources/app/node_modules/lodash/package.json +0 -0
  40. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/update-demo/v1/resources/app/package.json +0 -0
  41. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/update-demo/v2/resources/app/main.js +0 -0
  42. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/update-demo/v2/resources/app/node_modules/axios/package.json +0 -0
  43. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/update-demo/v2/resources/app/node_modules/fast-native/build/Release/addon.node +0 -0
  44. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/update-demo/v2/resources/app/node_modules/lodash/package.json +0 -0
  45. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/update-demo/v2/resources/app/package.json +0 -0
  46. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/vulnerable-demo/dotnet/IntakeDemo.deps.json +0 -0
  47. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/vulnerable-demo/dotnet/IntakeDemo.runtimeconfig.json +0 -0
  48. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/examples/vulnerable-demo/resources/app.asar +0 -0
  49. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/src/intake_gatekeeper/__main__.py +0 -0
  50. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/src/intake_gatekeeper/asar.py +0 -0
  51. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/src/intake_gatekeeper/cli.py +0 -0
  52. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/src/intake_gatekeeper/electron.py +0 -0
  53. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/src/intake_gatekeeper/golang.py +0 -0
  54. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/src/intake_gatekeeper/osv.py +0 -0
  55. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/src/intake_gatekeeper/policy.py +0 -0
  56. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/src/intake_gatekeeper/py.typed +0 -0
  57. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/src/intake_gatekeeper/rules.py +0 -0
  58. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/tests/conftest.py +0 -0
  59. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/tests/test_asar.py +0 -0
  60. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/tests/test_cli.py +0 -0
  61. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/tests/test_diff.py +0 -0
  62. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/tests/test_electron.py +0 -0
  63. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/tests/test_golang.py +0 -0
  64. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/tests/test_live_osv.py +0 -0
  65. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/tests/test_osv.py +0 -0
  66. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/tests/test_output.py +0 -0
  67. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/tests/test_policy.py +0 -0
  68. {software_intake_gatekeeper-0.2.2 → software_intake_gatekeeper-0.3.0}/tests/test_scanner.py +0 -0
@@ -13,3 +13,4 @@ examples/vulnerable-demo/bin/
13
13
  tests/.go-fixtures/
14
14
  reports/
15
15
  intake-gatekeeper.*
16
+ examples/rust-demo/target/
@@ -3,6 +3,28 @@
3
3
  All notable changes are recorded here. The project follows [Semantic Versioning](https://semver.org/);
4
4
  while the major version is 0, minor releases may change behaviour.
5
5
 
6
+ ## [0.3.0] - 2026-10-18
7
+
8
+ ### Added
9
+
10
+ - Rust inventory from `cargo auditable` data (`.dep-v0` section) in ELF, PE and Mach-O, including
11
+ universal binaries, through a new bounded, dependency-free object-file reader. Build-only crates,
12
+ the root package and non-crates.io sources are listed but not sent to OSV.
13
+ - Python inventory from `*.dist-info/METADATA` and `*.egg-info` metadata (`pkg:pypi`, PEP 503 names).
14
+ - Maven inventory from `pom.properties` inside `.jar`, `.war` and `.ear` files, including nested
15
+ archives up to two levels (Spring Boot, WAR) read in memory with size and entry limits.
16
+ - Authenticode signer (subject, issuer, SHA-1 thumbprint) for PE files, without verification.
17
+ - Update check: removed signatures and a different signer on the same path need REVIEW; renewed
18
+ certificates from the same signer and newly signed files are listed only.
19
+ - Real-artifact CI job on Linux, Windows and macOS: `cargo auditable` builds compared with
20
+ `rust-audit-info`, Maven Central and PyPI artifacts with live OSV, and Windows'
21
+ `Get-AuthenticodeSignature`.
22
+
23
+ ### Changed
24
+
25
+ - The `nothing-recognized` gap now says how many binaries were fingerprinted without dependency data.
26
+ - Files that start with `MZ` but have no PE header are treated as non-PE instead of malformed.
27
+
6
28
  ## [0.2.2] - 2026-10-15
7
29
 
8
30
  ### Fixed
@@ -63,6 +85,7 @@ First public release and first GitHub Marketplace listing.
63
85
  - No Rust `cargo auditable`, Python wheel, Java or native C/C++ inventories yet.
64
86
  - Electron code rules detect literal settings only.
65
87
 
88
+ [0.3.0]: https://github.com/Sushant-Nemade/software-intake-gatekeeper/releases/tag/v0.3.0
66
89
  [0.2.2]: https://github.com/Sushant-Nemade/software-intake-gatekeeper/releases/tag/v0.2.2
67
90
  [0.2.1]: https://github.com/Sushant-Nemade/software-intake-gatekeeper/releases/tag/v0.2.1
68
91
  [0.2.0]: https://github.com/Sushant-Nemade/software-intake-gatekeeper/releases/tag/v0.2.0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: software-intake-gatekeeper
3
- Version: 0.2.2
4
- Summary: Local, evidence-first intake checks for third-party apps and binaries: SBOM, known vulnerabilities and Electron hardening.
3
+ Version: 0.3.0
4
+ Summary: Local, evidence-first intake checks for third-party apps and binaries: SBOM (Go, Rust, npm, NuGet, Python, Maven), known vulnerabilities, Electron hardening and code-signing changes.
5
5
  Project-URL: Homepage, https://github.com/Sushant-Nemade/software-intake-gatekeeper
6
6
  Project-URL: Source, https://github.com/Sushant-Nemade/software-intake-gatekeeper
7
7
  Project-URL: Changelog, https://github.com/Sushant-Nemade/software-intake-gatekeeper/blob/main/CHANGELOG.md
@@ -10,7 +10,7 @@ Project-URL: GitHub Marketplace, https://github.com/marketplace/actions/software
10
10
  Author: Sushant Nemade
11
11
  License-Expression: MIT
12
12
  License-File: LICENSE
13
- Keywords: cyclonedx,electron,osv,sarif,sbom,security,supply-chain
13
+ Keywords: authenticode,cargo-auditable,cyclonedx,electron,maven,osv,sarif,sbom,security,supply-chain
14
14
  Classifier: Development Status :: 3 - Alpha
15
15
  Classifier: Environment :: Console
16
16
  Classifier: Intended Audience :: Developers
@@ -28,6 +28,7 @@ Requires-Dist: httpx<1,>=0.28
28
28
  Requires-Dist: truststore>=0.10
29
29
  Provides-Extra: dev
30
30
  Requires-Dist: build>=1.2; extra == 'dev'
31
+ Requires-Dist: cryptography>=44; extra == 'dev'
31
32
  Requires-Dist: cyclonedx-python-lib[json-validation]>=9; extra == 'dev'
32
33
  Requires-Dist: mypy>=1.13; extra == 'dev'
33
34
  Requires-Dist: pip-audit>=2.7; extra == 'dev'
@@ -45,28 +46,33 @@ Description-Content-Type: text/markdown
45
46
 
46
47
  **Before you install, approve or ship a third-party app, find out what is inside it.**
47
48
 
48
- Software Intake Gatekeeper reads an installed application, an Electron `.asar` archive or a
49
- compiled binary. It never runs or unpacks it. It produces:
49
+ Software Intake Gatekeeper reads an installed application, an Electron `.asar` archive, a Java
50
+ archive or a compiled binary. It never runs or unpacks it. It produces:
50
51
 
51
- - a **CycloneDX 1.6 SBOM** of the components that are actually bundled,
52
+ - a **CycloneDX 1.6 SBOM** of the components that are actually bundled: Go, Rust, npm, NuGet,
53
+ Python and Maven packages, read from evidence inside the files,
52
54
  - **known vulnerabilities** for those exact versions from [OSV.dev](https://osv.dev) (only when you opt in),
53
55
  - **Electron hardening checks** for the fuse wire inside the executable and risky `BrowserWindow`
54
56
  settings in the app code,
57
+ - the **Authenticode signer** of each Windows executable,
55
58
  - an **APPROVE / REVIEW / REJECT** decision from a versioned policy, with expiring waivers,
56
59
  - an **update check** that compares two versions and gates on what the update *adds*: new
57
- vulnerabilities, weakened Electron fuses, new network endpoints, new native modules,
60
+ vulnerabilities, weakened Electron fuses, new network endpoints, new native modules, removed
61
+ signatures and a different publisher,
58
62
  - **SARIF** for GitHub code scanning, a **Markdown** summary for pull requests and a full **JSON** evidence report.
59
63
 
60
64
  Every finding names the file and, for scripts, the line, column and snippet that produced it.
61
65
  Anything the scanner could not verify is reported as a **coverage gap**. A blocking gap can
62
66
  never lead to APPROVE: an unknown result is not treated as safe.
63
67
 
64
- > Status: [v0.2.2](https://github.com/Sushant-Nemade/software-intake-gatekeeper/releases/tag/v0.2.2),
68
+ > Status: [v0.3.0](https://github.com/Sushant-Nemade/software-intake-gatekeeper/releases/tag/v0.3.0),
65
69
  > an alpha release on [PyPI](https://pypi.org/project/software-intake-gatekeeper/) and the
66
- > [GitHub Marketplace](https://github.com/marketplace/actions/software-intake-gatekeeper). The CLI is
67
- > tested on Windows and Linux with real Go binaries in three object formats, an ASAR archive packed
68
- > by the official `@electron/asar` tool and the live OSV API; the action also runs on macOS. The
69
- > results are static, version-level evidence, not proof of exploitability.
70
+ > [GitHub Marketplace](https://github.com/marketplace/actions/software-intake-gatekeeper). Every
71
+ > parser is checked against real artifacts: Go binaries in three object formats, `cargo auditable`
72
+ > builds compared with the official `rust-audit-info` decoder on Linux, Windows and macOS, JARs from
73
+ > Maven Central, packages installed by pip, signed executables compared with Windows'
74
+ > `Get-AuthenticodeSignature`, and the live OSV API. The results are static, version-level
75
+ > evidence, not proof of exploitability.
70
76
 
71
77
  Part of a small **software trust toolkit** with
72
78
  [Privacy Flow Inspector](https://github.com/Sushant-Nemade/privacy-flow-inspector): this tool checks
@@ -79,6 +85,8 @@ what is **inside** an app; the inspector checks which personal data the app **se
79
85
  | Who | Situation | What they get |
80
86
  |---|---|---|
81
87
  | IT / security intake | An employee requests a desktop tool (an Electron or Go CLI) | An SBOM, known CVEs and Electron hardening issues before approval, with a decision a reviewer can sign off |
88
+ | Platform and backend teams | A vendor ships a Spring Boot service, a Python tool or a Rust CLI | Maven coordinates from nested JARs (Log4Shell is found inside `BOOT-INF/lib`), Python package versions and Rust crates, each with advisories |
89
+ | Patch management | A new version of an approved app arrives | An update check that flags a removed signature or a different publisher before the rollout |
82
90
  | Product teams shipping Electron | A release candidate is ready | A CI gate that fails when `contextIsolation` is off, `RunAsNode` is enabled or bundled packages have critical advisories |
83
91
  | Supply-chain / compliance (EU CRA, medical-device SBOM expectations) | A vendor delivers a binary without an SBOM | A CycloneDX SBOM built from evidence inside the binary, with occurrences pointing to the source files |
84
92
  | Procurement / vendor review | Comparing two vendor builds | Reproducible JSON and SBOM output that can be compared across versions |
@@ -94,9 +102,9 @@ python -m pip install software-intake-gatekeeper
94
102
  To verify build provenance first, install the wheel from the GitHub release instead:
95
103
 
96
104
  ```powershell
97
- gh release download v0.2.2 -R Sushant-Nemade/software-intake-gatekeeper -p "*.whl"
98
- gh attestation verify software_intake_gatekeeper-0.2.2-py3-none-any.whl -R Sushant-Nemade/software-intake-gatekeeper
99
- python -m pip install software_intake_gatekeeper-0.2.2-py3-none-any.whl
105
+ gh release download v0.3.0 -R Sushant-Nemade/software-intake-gatekeeper -p "*.whl"
106
+ gh attestation verify software_intake_gatekeeper-0.3.0-py3-none-any.whl -R Sushant-Nemade/software-intake-gatekeeper
107
+ python -m pip install software_intake_gatekeeper-0.3.0-py3-none-any.whl
100
108
  ```
101
109
 
102
110
  Or install from a clone (needed for the demo below):
@@ -192,23 +200,29 @@ Components changed: 2 | endpoints +1/-0 | binaries +1/-0/~0
192
200
  | Electron | Runtime version change and every fuse change; a fuse moving into its risky state is **weakened** |
193
201
  | Network endpoints | Hosts of `http(s)://` and `ws(s)://` URLs that start a string literal in app code (outside `node_modules`); URLs in comments and license headers, XML namespaces and local addresses are skipped |
194
202
  | Executables and native modules | Added, removed or changed (SHA-256) PE, ELF and Mach-O files, including `.node` modules |
203
+ | Code signing | For the same file path: signature **removed**, **signer-changed** (different subject), `certificate-renewed` (same subject, new certificate) or `added` |
195
204
 
196
205
  **REJECT** if a new finding reaches `reject_at`. **REVIEW** for new findings at `review_at`, weakened
197
- fuses, new endpoints, new binaries, new components or blocking gaps in the new version.
198
- **APPROVE** otherwise. The SARIF output contains only the new findings.
206
+ fuses, new endpoints, new binaries, new components, removed signatures, a different signer or
207
+ blocking gaps in the new version. **APPROVE** otherwise. The SARIF output contains only the new
208
+ findings.
199
209
 
200
210
  ## What it inspects
201
211
 
202
212
  | Evidence | Source inside the target | Output |
203
213
  |---|---|---|
204
214
  | Go modules and toolchain | Build info embedded by the Go linker (Go 1.18+) in PE, ELF and Mach-O executables | `pkg:golang/...` components, including `stdlib` and `replace` directives |
215
+ | Rust crates | The zlib-compressed `.dep-v0` section that [`cargo auditable`](https://github.com/rust-secure-code/cargo-auditable) embeds in ELF, PE and Mach-O (including universal) binaries | `pkg:cargo/...` components; build-only crates and non-crates.io sources are listed but not queried |
216
+ | Python packages | `*.dist-info/METADATA` and `*.egg-info` (`PKG-INFO`) on disk, for example a bundled `site-packages` | `pkg:pypi/...` components with PEP 503 names |
217
+ | Maven artifacts | `META-INF/maven/**/pom.properties` in `.jar`, `.war` and `.ear` files, including nested archives such as Spring Boot `BOOT-INF/lib` and `WEB-INF/lib` (two levels, read in memory) | `pkg:maven/...` components, also for shaded dependencies |
205
218
  | npm packages | `node_modules/**/package.json` on disk and inside `.asar` archives | `pkg:npm/...` components; the app's own manifest is recorded but not queried |
206
219
  | NuGet packages | `*.deps.json` (`package` and self-contained `runtimepack` entries) | `pkg:nuget/...` components |
207
220
  | .NET shared framework | `*.runtimeconfig.json` | Minimum framework version, plus a note that the host runtime decides the patch level |
208
221
  | Electron runtime | The fuse sentinel and `Electron/x.y.z` string in the executable | Electron version (queried as npm `electron`) and the fuse states |
209
222
  | Electron app code | `.js/.cjs/.mjs/.ts` files outside `node_modules`, on disk or in `.asar` | `webPreferences` rule findings with line, column and snippet |
210
223
  | Network endpoints | `http(s)://` and `ws(s)://` hosts referenced by that app code | Host list with up to three source files each (used by the update check) |
211
- | Executables and native modules | PE, ELF and Mach-O files, including `.node` add-ons | Path, size, SHA-256 and kind (Go, Electron, native) |
224
+ | Executables and native modules | PE, ELF and Mach-O files, including `.node` add-ons | Path, size, SHA-256 and kind (Go, Rust, Electron, native) |
225
+ | Code signing | The Authenticode certificate table of PE files | Claimed signer subject, issuer and SHA-1 thumbprint (the value Windows shows); not cryptographically verified |
212
226
 
213
227
  ### Electron rules
214
228
 
@@ -354,23 +368,32 @@ and a `SHA256SUMS` file.
354
368
  | Check | How |
355
369
  |---|---|
356
370
  | Go build info decoding | Compared with `go version -m` on real binaries built in CI for Windows PE, Linux ELF and macOS Mach-O |
371
+ | Rust `cargo auditable` | CI builds a fixture with `cargo auditable` 0.7.7 on Linux, Windows and macOS and requires the same crates as the official `rust-audit-info` 0.5.4 decoder, then finds RUSTSEC-2021-0003 through live OSV. Locally, Alpine Linux's official `ripgrep` 14.1.1 binary decoded to 42 crates, every one matching ripgrep's `Cargo.lock` |
372
+ | Maven and Python | CI downloads `log4j-core` 2.14.1 from Maven Central (SHA-1 checked), nests it in a Spring Boot style JAR and requires Log4Shell (CVE-2021-44228); pip installs `requests` 2.25.0 and CVE-2023-32681 must be found. Locally, `jackson-databind` 2.9.10 nested in a fat JAR returned 49 advisories |
373
+ | Authenticode | CI compares signer and thumbprint with Windows' `Get-AuthenticodeSignature` for a signed executable. Locally identical for VS Code, Node.js, Git and uv; catalog-signed and unsigned files report no embedded signature |
357
374
  | ASAR parsing | Fixture packed by `@electron/asar` 4.3.1; listing matches the official tool |
358
375
  | Fuse decoding | Wire layout taken from `@electron/fuses`. A read-only local check on an installed Electron app (VS Code) matched `npx @electron/fuses read` for all 9 fuses |
359
376
  | Output formats | CycloneDX validated with `cyclonedx-python-lib` against the 1.6 schema in tests. SARIF validated against the OASIS 2.1.0 schema during development |
360
377
  | OSV client | Mocked transport tests (batching, pagination, retries, alias grouping, withdrawn advisories) plus an opt-in live test (`INTAKE_LIVE_OSV=1`) |
361
378
  | Scale | Read-only scan of a locally installed VS Code: 3,243 files and 5,451 archive entries in about 19 s with a warm file cache |
362
379
  | Real update | Update check of the open-source editor MarkText 0.21.1 to 0.21.2 (official Windows zips, checksums verified; 20,072 ASAR entries, 505 npm packages, live OSV) in about 30 s. It showed 27 resolved advisories, 49 new packages and changed native modules. The first run also reported 10 new and 10 resolved code findings that were only a renamed bundle (`index-BBEVtY79.js` to `index-Cz8Eo_n9.js`), and 44 new endpoints taken from code comments; v0.2.2 fixed both, leaving 0 new findings and 1 new endpoint |
363
- | CI | Ubuntu (Python 3.11 and 3.13) and Windows (3.13): pytest, ruff, mypy `--strict`, build, twine, pip-audit. The action itself runs on Ubuntu, Windows and macOS against the demo with live OSV, plus fail-on, invalid-input and update-check (`compare-to`) runs |
380
+ | CI | Ubuntu (Python 3.11 and 3.13) and Windows (3.13): pytest, ruff, mypy `--strict`, build, twine, pip-audit. A real-artifact job on Ubuntu, Windows and macOS. The action itself runs on Ubuntu, Windows and macOS against the demo with live OSV, plus fail-on, invalid-input and update-check (`compare-to`) runs |
364
381
 
365
- No third-party application files are committed. The local real-app checks are listed for
366
- transparency only.
382
+ No third-party application files are committed. Unit tests build minimal ELF, PE and Mach-O files
383
+ and sign them with throwaway certificates; real artifacts are downloaded and checked in CI.
367
384
 
368
385
  ## Limitations
369
386
 
370
387
  - Version matching is not reachability. A vulnerable function may never be called; use waivers
371
388
  with reasons when you have checked this.
372
- - It reads Go build info only for Go 1.18+ (older binaries are reported as gaps). It does not yet
373
- read Rust `cargo auditable` sections, Python wheels, Java JARs or native C/C++ libraries.
389
+ - It reads Go build info only for Go 1.18+ (older binaries are reported as gaps) and Rust crates
390
+ only from binaries built with `cargo auditable`. Native C/C++ libraries are fingerprinted but not
391
+ identified.
392
+ - Python packages are read from installed metadata folders, not from wheels, eggs or
393
+ PyInstaller/zipapp bundles. JARs without `pom.properties` are counted as a coverage note.
394
+ - The Authenticode check reports who a signature *claims* to be from. It does not verify the
395
+ signature, the certificate chain or revocation; use `Get-AuthenticodeSignature` or `signtool
396
+ verify` for that. Catalog signatures and macOS code signatures are not read.
374
397
  - Electron code rules are pattern-based. They catch literal settings, including minified ones,
375
398
  but not values computed at runtime.
376
399
  - Installers (`.exe`/`.msi`/`.dmg`) must be extracted or installed first. The tool does not unpack
@@ -380,12 +403,13 @@ transparency only.
380
403
 
381
404
  ## Roadmap
382
405
 
383
- 1. Rust `cargo auditable`, Python `*.dist-info` and Java `pom.properties` inventories.
384
- 2. Signature and publisher changes (Authenticode, code signing) in the update check.
406
+ 1. macOS code signatures (Team ID) in the update check.
407
+ 2. Python wheels and PyInstaller bundles; Gradle and `MANIFEST.MF`-only JARs.
385
408
  3. An optional adapter for reverse-engineering evidence tools behind an approval gate.
386
409
 
387
- Shipped from earlier roadmap items: the version-to-version update check (v0.2.0) and the
388
- PII-in-HAR inspector, released as the separate
410
+ Shipped from earlier roadmap items: Rust, Python and Maven inventories with Authenticode signer
411
+ changes (v0.3.0), the version-to-version update check (v0.2.0) and the PII-in-HAR inspector,
412
+ released as the separate
389
413
  [Privacy Flow Inspector](https://github.com/Sushant-Nemade/privacy-flow-inspector).
390
414
 
391
415
  ## Development
@@ -7,28 +7,33 @@
7
7
 
8
8
  **Before you install, approve or ship a third-party app, find out what is inside it.**
9
9
 
10
- Software Intake Gatekeeper reads an installed application, an Electron `.asar` archive or a
11
- compiled binary. It never runs or unpacks it. It produces:
10
+ Software Intake Gatekeeper reads an installed application, an Electron `.asar` archive, a Java
11
+ archive or a compiled binary. It never runs or unpacks it. It produces:
12
12
 
13
- - a **CycloneDX 1.6 SBOM** of the components that are actually bundled,
13
+ - a **CycloneDX 1.6 SBOM** of the components that are actually bundled: Go, Rust, npm, NuGet,
14
+ Python and Maven packages, read from evidence inside the files,
14
15
  - **known vulnerabilities** for those exact versions from [OSV.dev](https://osv.dev) (only when you opt in),
15
16
  - **Electron hardening checks** for the fuse wire inside the executable and risky `BrowserWindow`
16
17
  settings in the app code,
18
+ - the **Authenticode signer** of each Windows executable,
17
19
  - an **APPROVE / REVIEW / REJECT** decision from a versioned policy, with expiring waivers,
18
20
  - an **update check** that compares two versions and gates on what the update *adds*: new
19
- vulnerabilities, weakened Electron fuses, new network endpoints, new native modules,
21
+ vulnerabilities, weakened Electron fuses, new network endpoints, new native modules, removed
22
+ signatures and a different publisher,
20
23
  - **SARIF** for GitHub code scanning, a **Markdown** summary for pull requests and a full **JSON** evidence report.
21
24
 
22
25
  Every finding names the file and, for scripts, the line, column and snippet that produced it.
23
26
  Anything the scanner could not verify is reported as a **coverage gap**. A blocking gap can
24
27
  never lead to APPROVE: an unknown result is not treated as safe.
25
28
 
26
- > Status: [v0.2.2](https://github.com/Sushant-Nemade/software-intake-gatekeeper/releases/tag/v0.2.2),
29
+ > Status: [v0.3.0](https://github.com/Sushant-Nemade/software-intake-gatekeeper/releases/tag/v0.3.0),
27
30
  > an alpha release on [PyPI](https://pypi.org/project/software-intake-gatekeeper/) and the
28
- > [GitHub Marketplace](https://github.com/marketplace/actions/software-intake-gatekeeper). The CLI is
29
- > tested on Windows and Linux with real Go binaries in three object formats, an ASAR archive packed
30
- > by the official `@electron/asar` tool and the live OSV API; the action also runs on macOS. The
31
- > results are static, version-level evidence, not proof of exploitability.
31
+ > [GitHub Marketplace](https://github.com/marketplace/actions/software-intake-gatekeeper). Every
32
+ > parser is checked against real artifacts: Go binaries in three object formats, `cargo auditable`
33
+ > builds compared with the official `rust-audit-info` decoder on Linux, Windows and macOS, JARs from
34
+ > Maven Central, packages installed by pip, signed executables compared with Windows'
35
+ > `Get-AuthenticodeSignature`, and the live OSV API. The results are static, version-level
36
+ > evidence, not proof of exploitability.
32
37
 
33
38
  Part of a small **software trust toolkit** with
34
39
  [Privacy Flow Inspector](https://github.com/Sushant-Nemade/privacy-flow-inspector): this tool checks
@@ -41,6 +46,8 @@ what is **inside** an app; the inspector checks which personal data the app **se
41
46
  | Who | Situation | What they get |
42
47
  |---|---|---|
43
48
  | IT / security intake | An employee requests a desktop tool (an Electron or Go CLI) | An SBOM, known CVEs and Electron hardening issues before approval, with a decision a reviewer can sign off |
49
+ | Platform and backend teams | A vendor ships a Spring Boot service, a Python tool or a Rust CLI | Maven coordinates from nested JARs (Log4Shell is found inside `BOOT-INF/lib`), Python package versions and Rust crates, each with advisories |
50
+ | Patch management | A new version of an approved app arrives | An update check that flags a removed signature or a different publisher before the rollout |
44
51
  | Product teams shipping Electron | A release candidate is ready | A CI gate that fails when `contextIsolation` is off, `RunAsNode` is enabled or bundled packages have critical advisories |
45
52
  | Supply-chain / compliance (EU CRA, medical-device SBOM expectations) | A vendor delivers a binary without an SBOM | A CycloneDX SBOM built from evidence inside the binary, with occurrences pointing to the source files |
46
53
  | Procurement / vendor review | Comparing two vendor builds | Reproducible JSON and SBOM output that can be compared across versions |
@@ -56,9 +63,9 @@ python -m pip install software-intake-gatekeeper
56
63
  To verify build provenance first, install the wheel from the GitHub release instead:
57
64
 
58
65
  ```powershell
59
- gh release download v0.2.2 -R Sushant-Nemade/software-intake-gatekeeper -p "*.whl"
60
- gh attestation verify software_intake_gatekeeper-0.2.2-py3-none-any.whl -R Sushant-Nemade/software-intake-gatekeeper
61
- python -m pip install software_intake_gatekeeper-0.2.2-py3-none-any.whl
66
+ gh release download v0.3.0 -R Sushant-Nemade/software-intake-gatekeeper -p "*.whl"
67
+ gh attestation verify software_intake_gatekeeper-0.3.0-py3-none-any.whl -R Sushant-Nemade/software-intake-gatekeeper
68
+ python -m pip install software_intake_gatekeeper-0.3.0-py3-none-any.whl
62
69
  ```
63
70
 
64
71
  Or install from a clone (needed for the demo below):
@@ -154,23 +161,29 @@ Components changed: 2 | endpoints +1/-0 | binaries +1/-0/~0
154
161
  | Electron | Runtime version change and every fuse change; a fuse moving into its risky state is **weakened** |
155
162
  | Network endpoints | Hosts of `http(s)://` and `ws(s)://` URLs that start a string literal in app code (outside `node_modules`); URLs in comments and license headers, XML namespaces and local addresses are skipped |
156
163
  | Executables and native modules | Added, removed or changed (SHA-256) PE, ELF and Mach-O files, including `.node` modules |
164
+ | Code signing | For the same file path: signature **removed**, **signer-changed** (different subject), `certificate-renewed` (same subject, new certificate) or `added` |
157
165
 
158
166
  **REJECT** if a new finding reaches `reject_at`. **REVIEW** for new findings at `review_at`, weakened
159
- fuses, new endpoints, new binaries, new components or blocking gaps in the new version.
160
- **APPROVE** otherwise. The SARIF output contains only the new findings.
167
+ fuses, new endpoints, new binaries, new components, removed signatures, a different signer or
168
+ blocking gaps in the new version. **APPROVE** otherwise. The SARIF output contains only the new
169
+ findings.
161
170
 
162
171
  ## What it inspects
163
172
 
164
173
  | Evidence | Source inside the target | Output |
165
174
  |---|---|---|
166
175
  | Go modules and toolchain | Build info embedded by the Go linker (Go 1.18+) in PE, ELF and Mach-O executables | `pkg:golang/...` components, including `stdlib` and `replace` directives |
176
+ | Rust crates | The zlib-compressed `.dep-v0` section that [`cargo auditable`](https://github.com/rust-secure-code/cargo-auditable) embeds in ELF, PE and Mach-O (including universal) binaries | `pkg:cargo/...` components; build-only crates and non-crates.io sources are listed but not queried |
177
+ | Python packages | `*.dist-info/METADATA` and `*.egg-info` (`PKG-INFO`) on disk, for example a bundled `site-packages` | `pkg:pypi/...` components with PEP 503 names |
178
+ | Maven artifacts | `META-INF/maven/**/pom.properties` in `.jar`, `.war` and `.ear` files, including nested archives such as Spring Boot `BOOT-INF/lib` and `WEB-INF/lib` (two levels, read in memory) | `pkg:maven/...` components, also for shaded dependencies |
167
179
  | npm packages | `node_modules/**/package.json` on disk and inside `.asar` archives | `pkg:npm/...` components; the app's own manifest is recorded but not queried |
168
180
  | NuGet packages | `*.deps.json` (`package` and self-contained `runtimepack` entries) | `pkg:nuget/...` components |
169
181
  | .NET shared framework | `*.runtimeconfig.json` | Minimum framework version, plus a note that the host runtime decides the patch level |
170
182
  | Electron runtime | The fuse sentinel and `Electron/x.y.z` string in the executable | Electron version (queried as npm `electron`) and the fuse states |
171
183
  | Electron app code | `.js/.cjs/.mjs/.ts` files outside `node_modules`, on disk or in `.asar` | `webPreferences` rule findings with line, column and snippet |
172
184
  | Network endpoints | `http(s)://` and `ws(s)://` hosts referenced by that app code | Host list with up to three source files each (used by the update check) |
173
- | Executables and native modules | PE, ELF and Mach-O files, including `.node` add-ons | Path, size, SHA-256 and kind (Go, Electron, native) |
185
+ | Executables and native modules | PE, ELF and Mach-O files, including `.node` add-ons | Path, size, SHA-256 and kind (Go, Rust, Electron, native) |
186
+ | Code signing | The Authenticode certificate table of PE files | Claimed signer subject, issuer and SHA-1 thumbprint (the value Windows shows); not cryptographically verified |
174
187
 
175
188
  ### Electron rules
176
189
 
@@ -316,23 +329,32 @@ and a `SHA256SUMS` file.
316
329
  | Check | How |
317
330
  |---|---|
318
331
  | Go build info decoding | Compared with `go version -m` on real binaries built in CI for Windows PE, Linux ELF and macOS Mach-O |
332
+ | Rust `cargo auditable` | CI builds a fixture with `cargo auditable` 0.7.7 on Linux, Windows and macOS and requires the same crates as the official `rust-audit-info` 0.5.4 decoder, then finds RUSTSEC-2021-0003 through live OSV. Locally, Alpine Linux's official `ripgrep` 14.1.1 binary decoded to 42 crates, every one matching ripgrep's `Cargo.lock` |
333
+ | Maven and Python | CI downloads `log4j-core` 2.14.1 from Maven Central (SHA-1 checked), nests it in a Spring Boot style JAR and requires Log4Shell (CVE-2021-44228); pip installs `requests` 2.25.0 and CVE-2023-32681 must be found. Locally, `jackson-databind` 2.9.10 nested in a fat JAR returned 49 advisories |
334
+ | Authenticode | CI compares signer and thumbprint with Windows' `Get-AuthenticodeSignature` for a signed executable. Locally identical for VS Code, Node.js, Git and uv; catalog-signed and unsigned files report no embedded signature |
319
335
  | ASAR parsing | Fixture packed by `@electron/asar` 4.3.1; listing matches the official tool |
320
336
  | Fuse decoding | Wire layout taken from `@electron/fuses`. A read-only local check on an installed Electron app (VS Code) matched `npx @electron/fuses read` for all 9 fuses |
321
337
  | Output formats | CycloneDX validated with `cyclonedx-python-lib` against the 1.6 schema in tests. SARIF validated against the OASIS 2.1.0 schema during development |
322
338
  | OSV client | Mocked transport tests (batching, pagination, retries, alias grouping, withdrawn advisories) plus an opt-in live test (`INTAKE_LIVE_OSV=1`) |
323
339
  | Scale | Read-only scan of a locally installed VS Code: 3,243 files and 5,451 archive entries in about 19 s with a warm file cache |
324
340
  | Real update | Update check of the open-source editor MarkText 0.21.1 to 0.21.2 (official Windows zips, checksums verified; 20,072 ASAR entries, 505 npm packages, live OSV) in about 30 s. It showed 27 resolved advisories, 49 new packages and changed native modules. The first run also reported 10 new and 10 resolved code findings that were only a renamed bundle (`index-BBEVtY79.js` to `index-Cz8Eo_n9.js`), and 44 new endpoints taken from code comments; v0.2.2 fixed both, leaving 0 new findings and 1 new endpoint |
325
- | CI | Ubuntu (Python 3.11 and 3.13) and Windows (3.13): pytest, ruff, mypy `--strict`, build, twine, pip-audit. The action itself runs on Ubuntu, Windows and macOS against the demo with live OSV, plus fail-on, invalid-input and update-check (`compare-to`) runs |
341
+ | CI | Ubuntu (Python 3.11 and 3.13) and Windows (3.13): pytest, ruff, mypy `--strict`, build, twine, pip-audit. A real-artifact job on Ubuntu, Windows and macOS. The action itself runs on Ubuntu, Windows and macOS against the demo with live OSV, plus fail-on, invalid-input and update-check (`compare-to`) runs |
326
342
 
327
- No third-party application files are committed. The local real-app checks are listed for
328
- transparency only.
343
+ No third-party application files are committed. Unit tests build minimal ELF, PE and Mach-O files
344
+ and sign them with throwaway certificates; real artifacts are downloaded and checked in CI.
329
345
 
330
346
  ## Limitations
331
347
 
332
348
  - Version matching is not reachability. A vulnerable function may never be called; use waivers
333
349
  with reasons when you have checked this.
334
- - It reads Go build info only for Go 1.18+ (older binaries are reported as gaps). It does not yet
335
- read Rust `cargo auditable` sections, Python wheels, Java JARs or native C/C++ libraries.
350
+ - It reads Go build info only for Go 1.18+ (older binaries are reported as gaps) and Rust crates
351
+ only from binaries built with `cargo auditable`. Native C/C++ libraries are fingerprinted but not
352
+ identified.
353
+ - Python packages are read from installed metadata folders, not from wheels, eggs or
354
+ PyInstaller/zipapp bundles. JARs without `pom.properties` are counted as a coverage note.
355
+ - The Authenticode check reports who a signature *claims* to be from. It does not verify the
356
+ signature, the certificate chain or revocation; use `Get-AuthenticodeSignature` or `signtool
357
+ verify` for that. Catalog signatures and macOS code signatures are not read.
336
358
  - Electron code rules are pattern-based. They catch literal settings, including minified ones,
337
359
  but not values computed at runtime.
338
360
  - Installers (`.exe`/`.msi`/`.dmg`) must be extracted or installed first. The tool does not unpack
@@ -342,12 +364,13 @@ transparency only.
342
364
 
343
365
  ## Roadmap
344
366
 
345
- 1. Rust `cargo auditable`, Python `*.dist-info` and Java `pom.properties` inventories.
346
- 2. Signature and publisher changes (Authenticode, code signing) in the update check.
367
+ 1. macOS code signatures (Team ID) in the update check.
368
+ 2. Python wheels and PyInstaller bundles; Gradle and `MANIFEST.MF`-only JARs.
347
369
  3. An optional adapter for reverse-engineering evidence tools behind an approval gate.
348
370
 
349
- Shipped from earlier roadmap items: the version-to-version update check (v0.2.0) and the
350
- PII-in-HAR inspector, released as the separate
371
+ Shipped from earlier roadmap items: Rust, Python and Maven inventories with Authenticode signer
372
+ changes (v0.3.0), the version-to-version update check (v0.2.0) and the PII-in-HAR inspector,
373
+ released as the separate
351
374
  [Privacy Flow Inspector](https://github.com/Sushant-Nemade/privacy-flow-inspector).
352
375
 
353
376
  ## Development
@@ -0,0 +1,16 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "rust-demo"
7
+ version = "0.1.0"
8
+ dependencies = [
9
+ "smallvec",
10
+ ]
11
+
12
+ [[package]]
13
+ name = "smallvec"
14
+ version = "1.6.0"
15
+ source = "registry+https://github.com/rust-lang/crates.io-index"
16
+ checksum = "1a55ca5f3b68e41c979bf8c46a6f1da892ca4db8f94023ce0bd32407573b1ac0"
@@ -0,0 +1,10 @@
1
+ [package]
2
+ name = "rust-demo"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ publish = false
6
+ description = "Fictional fixture for Software Intake Gatekeeper; smallvec 1.6.0 is pinned on purpose (RUSTSEC-2021-0003)."
7
+ license = "MIT"
8
+
9
+ [dependencies]
10
+ smallvec = "=1.6.0"
@@ -0,0 +1,7 @@
1
+ use smallvec::SmallVec;
2
+
3
+ fn main() {
4
+ let mut values: SmallVec<[u8; 4]> = SmallVec::new();
5
+ values.extend_from_slice(b"intake");
6
+ println!("fixture for Software Intake Gatekeeper: {} bytes", values.len());
7
+ }
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "software-intake-gatekeeper"
7
- version = "0.2.2"
8
- description = "Local, evidence-first intake checks for third-party apps and binaries: SBOM, known vulnerabilities and Electron hardening."
7
+ version = "0.3.0"
8
+ description = "Local, evidence-first intake checks for third-party apps and binaries: SBOM (Go, Rust, npm, NuGet, Python, Maven), known vulnerabilities, Electron hardening and code-signing changes."
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
11
  license-files = ["LICENSE"]
12
12
  requires-python = ">=3.11"
13
13
  authors = [{ name = "Sushant Nemade" }]
14
- keywords = ["sbom", "cyclonedx", "sarif", "osv", "electron", "supply-chain", "security"]
14
+ keywords = ["sbom", "cyclonedx", "sarif", "osv", "electron", "supply-chain", "security", "cargo-auditable", "maven", "authenticode"]
15
15
  classifiers = [
16
16
  "Development Status :: 3 - Alpha",
17
17
  "Environment :: Console",
@@ -34,6 +34,7 @@ dependencies = [
34
34
  [project.optional-dependencies]
35
35
  dev = [
36
36
  "build>=1.2",
37
+ "cryptography>=44",
37
38
  "cyclonedx-python-lib[json-validation]>=9",
38
39
  "mypy>=1.13",
39
40
  "pip-audit>=2.7",
@@ -1,3 +1,3 @@
1
1
  """Software Intake Gatekeeper: evidence-first intake checks for third-party software."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.3.0"
@@ -0,0 +1,149 @@
1
+ """Read the Authenticode signer identity from a PE file without verifying it.
2
+
3
+ Only identifies who the embedded signature claims to be from. Cryptographic validity, chain trust,
4
+ revocation and catalog signatures are outside this module.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import struct
11
+ from dataclasses import dataclass
12
+
13
+ from intake_gatekeeper import objfile
14
+ from intake_gatekeeper.golang import Bytes
15
+
16
+ _MAX_TABLE = 16 * 1024 * 1024
17
+ _SIGNED_DATA = bytes.fromhex("2a864886f70d010702")
18
+ _OID_CN = bytes.fromhex("550403")
19
+ _OID_O = bytes.fromhex("55040a")
20
+ _STRING_TAGS = {
21
+ 0x0C: "utf-8",
22
+ 0x13: "ascii",
23
+ 0x16: "ascii",
24
+ 0x14: "latin-1",
25
+ 0x1E: "utf-16-be",
26
+ 0x1C: "utf-32-be",
27
+ }
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class Signer:
32
+ subject: str
33
+ issuer: str
34
+ serial: str
35
+ sha1: str
36
+ sha256: str
37
+
38
+
39
+ class SignatureError(ValueError):
40
+ pass
41
+
42
+
43
+ def _tlv(data: bytes, pos: int, end: int) -> tuple[int, int, int]:
44
+ """Return (tag, content start, content end) of the DER element at pos."""
45
+ if pos + 2 > end:
46
+ raise SignatureError("truncated DER element")
47
+ tag, first = data[pos], data[pos + 1]
48
+ pos += 2
49
+ if first < 0x80:
50
+ length = first
51
+ else:
52
+ count = first & 0x7F
53
+ if count == 0 or count > 4 or pos + count > end:
54
+ raise SignatureError("unsupported DER length")
55
+ length = int.from_bytes(data[pos : pos + count], "big")
56
+ pos += count
57
+ if pos + length > end:
58
+ raise SignatureError("DER element exceeds its container")
59
+ return tag, pos, pos + length
60
+
61
+
62
+ def _children(data: bytes, start: int, end: int) -> list[tuple[int, int, int, int]]:
63
+ """(tag, content start, content end, element start) for each element in a range."""
64
+ items = []
65
+ while start < end:
66
+ tag, inner, stop = _tlv(data, start, end)
67
+ items.append((tag, inner, stop, start))
68
+ start = stop
69
+ return items
70
+
71
+
72
+ def _name(data: bytes, start: int, end: int) -> str:
73
+ parts: dict[bytes, str] = {}
74
+ for _set_tag, set_start, set_end, _ in _children(data, start, end):
75
+ for _seq_tag, seq_start, seq_end, _ in _children(data, set_start, set_end):
76
+ items = _children(data, seq_start, seq_end)
77
+ if len(items) != 2 or items[0][0] != 0x06:
78
+ continue
79
+ oid = data[items[0][1] : items[0][2]]
80
+ tag, value_start, value_end, _ = items[1]
81
+ encoding = _STRING_TAGS.get(tag)
82
+ if oid in (_OID_CN, _OID_O) and encoding and oid not in parts:
83
+ parts[oid] = data[value_start:value_end].decode(encoding, errors="replace")
84
+ labels = [f"CN={parts[_OID_CN]}"] if _OID_CN in parts else []
85
+ if _OID_O in parts:
86
+ labels.append(f"O={parts[_OID_O]}")
87
+ return ", ".join(labels) or "(no CN or O)"
88
+
89
+
90
+ def _certificate(data: bytes, start: int, end: int) -> tuple[bytes, bytes, bytes]:
91
+ """Return (serial, issuer name, subject name) contents of one certificate."""
92
+ _tag, tbs_start, tbs_end, _ = _children(data, start, end)[0]
93
+ fields = _children(data, tbs_start, tbs_end)
94
+ if fields and fields[0][0] == 0xA0:
95
+ fields = fields[1:]
96
+ if len(fields) < 5:
97
+ raise SignatureError("certificate is missing fields")
98
+ serial = data[fields[0][1] : fields[0][2]]
99
+ issuer = data[fields[2][1] : fields[2][2]]
100
+ subject = data[fields[4][1] : fields[4][2]]
101
+ return serial, issuer, subject
102
+
103
+
104
+ def parse_pkcs7(blob: bytes) -> Signer:
105
+ _tag, info_start, info_end = _tlv(blob, 0, len(blob))
106
+ info = _children(blob, info_start, info_end)
107
+ if len(info) < 2 or blob[info[0][1] : info[0][2]] != _SIGNED_DATA or info[1][0] != 0xA0:
108
+ raise SignatureError("not a PKCS#7 SignedData structure")
109
+ _tag, signed_start, signed_end, _ = _children(blob, info[1][1], info[1][2])[0]
110
+ parts = _children(blob, signed_start, signed_end)
111
+ certificates = next((p for p in parts if p[0] == 0xA0), None)
112
+ signer_infos = parts[-1]
113
+ if certificates is None or signer_infos[0] != 0x31:
114
+ raise SignatureError("SignedData has no certificates or signer")
115
+ signer_info = _children(blob, signer_infos[1], signer_infos[2])[0]
116
+ sid = _children(blob, signer_info[1], signer_info[2])[1]
117
+ if sid[0] != 0x30:
118
+ raise SignatureError("signer identified by key id, not issuer and serial")
119
+ issuer_tlv, serial_tlv = _children(blob, sid[1], sid[2])[:2]
120
+ wanted_issuer = blob[issuer_tlv[1] : issuer_tlv[2]]
121
+ wanted_serial = blob[serial_tlv[1] : serial_tlv[2]]
122
+ for _tag, cert_start, cert_end, cert_begin in _children(blob, certificates[1], certificates[2]):
123
+ serial, issuer, subject = _certificate(blob, cert_start, cert_end)
124
+ if serial == wanted_serial and issuer == wanted_issuer:
125
+ der = blob[cert_begin:cert_end]
126
+ return Signer(
127
+ subject=_name(subject, 0, len(subject)),
128
+ issuer=_name(issuer, 0, len(issuer)),
129
+ serial=serial.lstrip(b"\x00").hex().upper() or "00",
130
+ sha1=hashlib.sha1(der, usedforsecurity=False).hexdigest().upper(),
131
+ sha256=hashlib.sha256(der).hexdigest(),
132
+ )
133
+ raise SignatureError("signer certificate is not included in the signature")
134
+
135
+
136
+ def read_signer(data: Bytes) -> Signer | None:
137
+ """Signer of the first embedded Authenticode signature; None when the file has none."""
138
+ try:
139
+ table = objfile.pe_certificate_table(data)
140
+ except objfile.FormatError as error:
141
+ raise SignatureError(f"PE headers are malformed: {error}") from None
142
+ if table is None:
143
+ return None
144
+ if table.size > _MAX_TABLE or table.size < 8:
145
+ raise SignatureError("certificate table size is out of range")
146
+ length, _revision, kind = struct.unpack("<IHH", data[table.offset : table.offset + 8])
147
+ if kind != 0x0002 or not 8 < length <= table.size:
148
+ raise SignatureError("certificate table holds no PKCS#7 signature")
149
+ return parse_pkcs7(bytes(data[table.offset + 8 : table.offset + length]))