├── .envrc ├── .github ├── renovate.json └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── default.nix ├── examples ├── crane │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── flake.lock │ ├── flake.nix │ └── src │ │ └── main.rs ├── flakes │ ├── flake.lock │ └── flake.nix └── niv │ ├── default.nix │ └── nix │ ├── sources.json │ └── sources.nix ├── flake.lock ├── flake.nix ├── nix ├── build-bom.nix ├── buildtime-dependencies.nix ├── packages │ └── transformer.nix ├── passthru-vendored.nix ├── runtime-dependencies.nix └── tests │ └── default.nix └── rust └── transformer ├── Cargo.lock ├── Cargo.toml └── src ├── buildtime_input.rs ├── cli.rs ├── cyclonedx.rs ├── derivation.rs ├── hash.rs ├── main.rs ├── runtime_input.rs └── transform.rs /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | ":semanticCommitsDisabled", 5 | "group:all", 6 | "schedule:monthly", 7 | ":maintainLockFilesMonthly" 8 | ], 9 | "dependencyDashboard": false, 10 | "lockFileMaintenance": { 11 | "commitMessageAction": "Update", 12 | "extends": [ 13 | "group:all" 14 | ] 15 | }, 16 | "nix": { 17 | "enabled": true 18 | }, 19 | "separateMajorMinor": false 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "Run nix flake check" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | merge_group: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: DeterminateSystems/nix-installer-action@v16 15 | with: 16 | diagnostic-endpoint: "" 17 | source-url: "https://install.lix.systems/lix/lix-installer-x86_64-linux" 18 | - uses: DeterminateSystems/magic-nix-cache-action@v8 19 | with: 20 | diagnostic-endpoint: "" 21 | - run: nix flake check --log-format raw-with-logs -L 22 | 23 | env: 24 | FORCE_COLOR: 1 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .direnv/ 2 | result* 3 | .pre-commit-config.yaml 4 | 5 | # Rust 6 | **/*.rs.bk # These are backup files generated by rustfmt 7 | target/ 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.4.0 (unreleased) 4 | 5 | ### Added 6 | 7 | - Added the ability to extract patches from a derivation and include them in 8 | the SBOM. 9 | - Added the ability to include multiple source URLs as external references. 10 | 11 | ### Fixed 12 | 13 | - Fixed an issue where some components would receive an empty string as VCS 14 | external reference. 15 | 16 | ## 0.3.0 17 | 18 | ### Added 19 | 20 | - Added the ability to collect SBOMs from vendored dependencies (e.g. from Rust 21 | or Go dependencies). 22 | - Added the option `excludes` to `buildBom` to exclude store paths via regex 23 | patterns from the final SBOM. 24 | - Added the option `extraPaths` to `buildBom` to consider extra dependencies 25 | but still generating an SBOM for the original derivation. 26 | - Hashes of fixed output derivations are now included in the SBOM. 27 | - A derivation's `src` url and hash are now included in the SBOM. 28 | - Derivations' descriptions are now included in the SBOM. 29 | 30 | ### Changed 31 | 32 | - `doc` and `man` outputs are not included in the SBOM anymore. 33 | - Generate CycloneDX v1.5 SBOMs instead of v1.4. 34 | - The created SBOMS are now reproducible because they derive their serial 35 | number from a known input instead of randomly generating it. 36 | 37 | ### Fixed 38 | 39 | - Fixed cross-compilation for SBOMs. 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 nikstur 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bombon 2 | 3 | Automagically build CycloneDX Software Bills of Materials (SBOMs) for Nix packages! 4 | 5 | Bombon generates CycloneDX v1.5 SBOMs which aim to be compliant with: 6 | 7 | - The German [Technical Guideline TR-03183 v2.0.0][] of the Federal Office for Information 8 | Security (BSI) 9 | - The US [Executive Order 14028][] 10 | 11 | If you find that they aren't compliant in any way, please open an issue! 12 | 13 | [Technical Guideline TR-03183 v2.0.0]: https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TR03183/BSI-TR-03183-2-2_0_0.pdf?__blob=publicationFile&v=3 14 | [Executive Order 14028]: https://www.nist.gov/itl/executive-order-14028-improving-nations-cybersecurity/software-security-supply-chains-software-1 15 | 16 | ## Getting Started 17 | 18 | ### Flakes 19 | 20 | ```sh 21 | nix flake init -t github:nikstur/bombon 22 | ``` 23 | 24 | Or manually copy this to `flake.nix` in your repository: 25 | 26 | ```nix 27 | # file: flake.nix 28 | { 29 | inputs = { 30 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 31 | bombon.url = "github:nikstur/bombon"; 32 | bombon.inputs.nixpkgs.follows = "nixpkgs"; 33 | }; 34 | 35 | outputs = { self, nixpkgs, bombon }: 36 | let 37 | system = "x86_64-linux"; 38 | pkgs = import nixpkgs { inherit system; }; 39 | in 40 | { 41 | packages.${system}.default = bombon.lib.${system}.buildBom pkgs.hello { }; 42 | }; 43 | } 44 | ``` 45 | 46 | ### Niv 47 | 48 | ```sh 49 | niv init 50 | niv add nikstur/bombon 51 | ``` 52 | 53 | ```nix 54 | # file: default.nix 55 | let 56 | sources = import ./nix/sources.nix { }; 57 | pkgs = import sources.nixpkgs { }; 58 | bombon = import sources.bombon { inherit pkgs; }; 59 | in 60 | bombon.buildBom pkgs.hello { } 61 | ``` 62 | 63 | ## Vendored Dependencies 64 | 65 | Some language ecosystems in Nixpkgs (most notably Rust and Go) vendor 66 | dependencies. This means that not every dependency is its own derivation and 67 | thus bombon cannot record their information as it does with "normal" Nix 68 | dependencies. However, bombon can automatically read SBOMs generated by other 69 | tools (like `cargo-cyclonedx`) for the vendored dependencies from a passthru 70 | derivation called `bombonVendoredSbom`. 71 | 72 | You can use the `passthruVendoredSbom.rust` function to add the 73 | `bombonVendoredSbom` passthru derivation to a Rust package: 74 | 75 | ```nix 76 | myPackageWithSbom = bombon.passthruVendoredSbom.rust myPackage { inherit pkgs; }; 77 | ``` 78 | 79 | Or using Flakes: 80 | 81 | ```nix 82 | myPackageWithSbom = bombon.lib.${system}.passthruVendoredSbom.rust myPackage { inherit pkgs; }; 83 | ``` 84 | 85 | An SBOM built from this new derivation will now include the vendored dependencies. 86 | 87 | ## Options 88 | 89 | `buildBom` accepts options as an attribute set. All attributes are optional: 90 | 91 | - `extraPaths`: a list of store paths to also consider for the SBOM. This is 92 | useful when you build images that discard their references (e.g. with 93 | [`unsafeDiscardReferences`](https://nixos.org/manual/nix/stable/language/advanced-attributes#adv-attr-unsafeDiscardReferences) 94 | but you still want their contents to appear in the SBOM. The `extraPaths` 95 | will appear as components of the main derivation. 96 | - `includeBuildtimeDependencies`: boolean flag to include buildtime dependencies in output. 97 | - `excludes`: a list of regex patterns of store paths to exclude from the final 98 | SBOM. 99 | 100 | Example: 101 | 102 | ```nix 103 | bombon.lib.${system}.buildBom pkgs.hello { 104 | extraPaths = [ pkgs.git ]; 105 | includeBuildtimeDependencies = true; 106 | excludes = [ "service" ]; 107 | } 108 | ``` 109 | 110 | `passthruVendoredSbom.rust` also accepts `includeBuildtimeDependencies` as an optional attribute. 111 | 112 | Example: 113 | 114 | ```nix 115 | myPackageWithSbom = bombon.passthruVendoredSbom.rust myPackage { inherit pkgs; includeBuildtimeDependencies = true; }; 116 | ``` 117 | 118 | ## Contributing 119 | 120 | During development, the Nix Repl is a convenient and quick way to test changes. 121 | Start the repl, loading your local version of nixpkgs. 122 | 123 | ```sh 124 | nix repl 125 | ``` 126 | 127 | Inside the repl, load the bombon flake and build the BOM for a package you 128 | are interested in. 129 | 130 | ```nix-repl 131 | :l . 132 | :b lib.x86_64-linux.buildBom python3 { } 133 | ``` 134 | 135 | Remember to re-load the bombon flake every time you made changes to any of the 136 | source code. 137 | 138 | ## Acknowledgements 139 | 140 | The way dependencies are retrieved using Nix is heavily influenced by this 141 | [blog article from Nicolas 142 | Mattia](https://www.nmattia.com/posts/2019-10-08-runtime-dependencies.html). 143 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: 2 | 3 | let 4 | passthruVendoredSbom = import ./nix/passthru-vendored.nix; 5 | transformerWithoutSbom = pkgs.callPackage ./nix/packages/transformer.nix { }; 6 | in 7 | rec { 8 | 9 | # It's useful to have these exposed for debugging. However, they are not a 10 | # public interface. 11 | __internal = { 12 | buildtimeDependencies = pkgs.callPackage ./nix/buildtime-dependencies.nix { }; 13 | runtimeDependencies = pkgs.callPackage ./nix/runtime-dependencies.nix { }; 14 | }; 15 | 16 | transformer = passthruVendoredSbom.rust transformerWithoutSbom { inherit pkgs; }; 17 | 18 | buildBom = pkgs.callPackage ./nix/build-bom.nix { 19 | inherit transformer; 20 | inherit (__internal) buildtimeDependencies runtimeDependencies; 21 | }; 22 | 23 | inherit passthruVendoredSbom; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /examples/crane/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "memchr" 16 | version = "2.7.4" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 19 | 20 | [[package]] 21 | name = "quick-start" 22 | version = "0.1.0" 23 | dependencies = [ 24 | "regex", 25 | ] 26 | 27 | [[package]] 28 | name = "regex" 29 | version = "1.10.5" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" 32 | dependencies = [ 33 | "aho-corasick", 34 | "memchr", 35 | "regex-automata", 36 | "regex-syntax", 37 | ] 38 | 39 | [[package]] 40 | name = "regex-automata" 41 | version = "0.4.7" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 44 | dependencies = [ 45 | "aho-corasick", 46 | "memchr", 47 | "regex-syntax", 48 | ] 49 | 50 | [[package]] 51 | name = "regex-syntax" 52 | version = "0.8.4" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 55 | -------------------------------------------------------------------------------- /examples/crane/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quick-start" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | regex = "1.10.5" 11 | -------------------------------------------------------------------------------- /examples/crane/README.md: -------------------------------------------------------------------------------- 1 | # Crane Rust Build SBOM Example 2 | 3 | This is a basic example on how to use `bombon` with a `crane`-based rust build environment. 4 | 5 | It's based on the [simple crane example](https://crane.dev/examples/quick-start-simple.html), 6 | only adding the necessary `bombon` features to generate sboms. 7 | 8 | It also adds a `regex` dependency and example code from the [cargo book](https://doc.rust-lang.org/cargo/guide/dependencies.html). 9 | 10 | You can build the binary with: 11 | 12 | ``` 13 | $ nix build 14 | ``` 15 | 16 | You can build the SBOM with: 17 | 18 | ``` 19 | $ nix build .#sbom 20 | ``` 21 | -------------------------------------------------------------------------------- /examples/crane/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "bombon": { 4 | "inputs": { 5 | "flake-parts": "flake-parts", 6 | "flake-utils": "flake-utils", 7 | "nixpkgs": [ 8 | "nixpkgs" 9 | ], 10 | "pre-commit-hooks-nix": "pre-commit-hooks-nix", 11 | "systems": "systems" 12 | }, 13 | "locked": { 14 | "lastModified": 1721119620, 15 | "narHash": "sha256-Z0TSvUUxyGCNPqchJaaHCQcT5DlUUUG6YEGoVomc3Mo=", 16 | "owner": "nikstur", 17 | "repo": "bombon", 18 | "rev": "35ce4edf3abf2a6b49f7b72f7dc5f05f700c8cc4", 19 | "type": "github" 20 | }, 21 | "original": { 22 | "owner": "nikstur", 23 | "repo": "bombon", 24 | "type": "github" 25 | } 26 | }, 27 | "crane": { 28 | "inputs": { 29 | "nixpkgs": [ 30 | "nixpkgs" 31 | ] 32 | }, 33 | "locked": { 34 | "lastModified": 1721058578, 35 | "narHash": "sha256-fs/PVa3H5dS1//4BjecWi3nitXm5fRObx0JxXIAo+JA=", 36 | "owner": "ipetkov", 37 | "repo": "crane", 38 | "rev": "17e5109bb1d9fb393d70fba80988f7d70d1ded1a", 39 | "type": "github" 40 | }, 41 | "original": { 42 | "owner": "ipetkov", 43 | "repo": "crane", 44 | "type": "github" 45 | } 46 | }, 47 | "flake-compat": { 48 | "flake": false, 49 | "locked": { 50 | "lastModified": 1696426674, 51 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 52 | "owner": "edolstra", 53 | "repo": "flake-compat", 54 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 55 | "type": "github" 56 | }, 57 | "original": { 58 | "owner": "edolstra", 59 | "repo": "flake-compat", 60 | "type": "github" 61 | } 62 | }, 63 | "flake-parts": { 64 | "inputs": { 65 | "nixpkgs-lib": [ 66 | "bombon", 67 | "nixpkgs" 68 | ] 69 | }, 70 | "locked": { 71 | "lastModified": 1719994518, 72 | "narHash": "sha256-pQMhCCHyQGRzdfAkdJ4cIWiw+JNuWsTX7f0ZYSyz0VY=", 73 | "owner": "hercules-ci", 74 | "repo": "flake-parts", 75 | "rev": "9227223f6d922fee3c7b190b2cc238a99527bbb7", 76 | "type": "github" 77 | }, 78 | "original": { 79 | "owner": "hercules-ci", 80 | "repo": "flake-parts", 81 | "type": "github" 82 | } 83 | }, 84 | "flake-utils": { 85 | "inputs": { 86 | "systems": [ 87 | "bombon", 88 | "systems" 89 | ] 90 | }, 91 | "locked": { 92 | "lastModified": 1710146030, 93 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 94 | "owner": "numtide", 95 | "repo": "flake-utils", 96 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 97 | "type": "github" 98 | }, 99 | "original": { 100 | "owner": "numtide", 101 | "repo": "flake-utils", 102 | "type": "github" 103 | } 104 | }, 105 | "flake-utils_2": { 106 | "inputs": { 107 | "systems": "systems_2" 108 | }, 109 | "locked": { 110 | "lastModified": 1710146030, 111 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 112 | "owner": "numtide", 113 | "repo": "flake-utils", 114 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 115 | "type": "github" 116 | }, 117 | "original": { 118 | "owner": "numtide", 119 | "repo": "flake-utils", 120 | "type": "github" 121 | } 122 | }, 123 | "gitignore": { 124 | "inputs": { 125 | "nixpkgs": [ 126 | "bombon", 127 | "pre-commit-hooks-nix", 128 | "nixpkgs" 129 | ] 130 | }, 131 | "locked": { 132 | "lastModified": 1709087332, 133 | "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 134 | "owner": "hercules-ci", 135 | "repo": "gitignore.nix", 136 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 137 | "type": "github" 138 | }, 139 | "original": { 140 | "owner": "hercules-ci", 141 | "repo": "gitignore.nix", 142 | "type": "github" 143 | } 144 | }, 145 | "nixpkgs": { 146 | "locked": { 147 | "lastModified": 1720957393, 148 | "narHash": "sha256-oedh2RwpjEa+TNxhg5Je9Ch6d3W1NKi7DbRO1ziHemA=", 149 | "owner": "NixOS", 150 | "repo": "nixpkgs", 151 | "rev": "693bc46d169f5af9c992095736e82c3488bf7dbb", 152 | "type": "github" 153 | }, 154 | "original": { 155 | "owner": "NixOS", 156 | "ref": "nixos-unstable", 157 | "repo": "nixpkgs", 158 | "type": "github" 159 | } 160 | }, 161 | "nixpkgs-stable": { 162 | "locked": { 163 | "lastModified": 1720386169, 164 | "narHash": "sha256-NGKVY4PjzwAa4upkGtAMz1npHGoRzWotlSnVlqI40mo=", 165 | "owner": "NixOS", 166 | "repo": "nixpkgs", 167 | "rev": "194846768975b7ad2c4988bdb82572c00222c0d7", 168 | "type": "github" 169 | }, 170 | "original": { 171 | "owner": "NixOS", 172 | "ref": "nixos-24.05", 173 | "repo": "nixpkgs", 174 | "type": "github" 175 | } 176 | }, 177 | "pre-commit-hooks-nix": { 178 | "inputs": { 179 | "flake-compat": "flake-compat", 180 | "gitignore": "gitignore", 181 | "nixpkgs": [ 182 | "bombon", 183 | "nixpkgs" 184 | ], 185 | "nixpkgs-stable": "nixpkgs-stable" 186 | }, 187 | "locked": { 188 | "lastModified": 1720524665, 189 | "narHash": "sha256-ni/87oHPZm6Gv0ECYxr1f6uxB0UKBWJ6HvS7lwLU6oY=", 190 | "owner": "cachix", 191 | "repo": "pre-commit-hooks.nix", 192 | "rev": "8d6a17d0cdf411c55f12602624df6368ad86fac1", 193 | "type": "github" 194 | }, 195 | "original": { 196 | "owner": "cachix", 197 | "repo": "pre-commit-hooks.nix", 198 | "type": "github" 199 | } 200 | }, 201 | "root": { 202 | "inputs": { 203 | "bombon": "bombon", 204 | "crane": "crane", 205 | "flake-utils": "flake-utils_2", 206 | "nixpkgs": "nixpkgs" 207 | } 208 | }, 209 | "systems": { 210 | "locked": { 211 | "lastModified": 1681028828, 212 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 213 | "owner": "nix-systems", 214 | "repo": "default", 215 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 216 | "type": "github" 217 | }, 218 | "original": { 219 | "owner": "nix-systems", 220 | "repo": "default", 221 | "type": "github" 222 | } 223 | }, 224 | "systems_2": { 225 | "locked": { 226 | "lastModified": 1681028828, 227 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 228 | "owner": "nix-systems", 229 | "repo": "default", 230 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 231 | "type": "github" 232 | }, 233 | "original": { 234 | "owner": "nix-systems", 235 | "repo": "default", 236 | "type": "github" 237 | } 238 | } 239 | }, 240 | "root": "root", 241 | "version": 7 242 | } 243 | -------------------------------------------------------------------------------- /examples/crane/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Build a cargo project without extra checks"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | 7 | crane = { 8 | url = "github:ipetkov/crane"; 9 | inputs.nixpkgs.follows = "nixpkgs"; 10 | }; 11 | 12 | flake-utils.url = "github:numtide/flake-utils"; 13 | 14 | bombon.url = "github:nikstur/bombon"; 15 | bombon.inputs.nixpkgs.follows = "nixpkgs"; 16 | }; 17 | 18 | outputs = 19 | { 20 | self, 21 | nixpkgs, 22 | crane, 23 | flake-utils, 24 | bombon, 25 | ... 26 | }: 27 | flake-utils.lib.eachDefaultSystem ( 28 | system: 29 | let 30 | pkgs = nixpkgs.legacyPackages.${system}; 31 | 32 | craneLib = crane.mkLib pkgs; 33 | 34 | # Common arguments can be set here to avoid repeating them later 35 | # Note: changes here will rebuild all dependency crates 36 | commonArgs = { 37 | src = craneLib.cleanCargoSource ./.; 38 | 39 | buildInputs = pkgs.lib.optionals pkgs.stdenv.isDarwin [ 40 | # Additional darwin specific inputs can be set here 41 | pkgs.libiconv 42 | ]; 43 | }; 44 | 45 | my-crate = bombon.lib.${system}.passthruVendoredSbom.rust (craneLib.buildPackage ( 46 | commonArgs 47 | // { 48 | cargoArtifacts = craneLib.buildDepsOnly commonArgs; 49 | } 50 | )) { inherit pkgs; }; 51 | in 52 | { 53 | checks = { 54 | inherit my-crate; 55 | }; 56 | 57 | packages.default = my-crate; 58 | packages.sbom = bombon.lib.${system}.buildBom my-crate { }; 59 | 60 | apps.default = flake-utils.lib.mkApp { 61 | drv = my-crate; 62 | }; 63 | 64 | devShells.default = craneLib.devShell { 65 | # Inherit inputs from checks. 66 | checks = self.checks.${system}; 67 | }; 68 | } 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /examples/crane/src/main.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | 3 | fn main() { 4 | let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap(); 5 | println!("Did our date match? {}", re.is_match("2014-01-01")); 6 | } 7 | -------------------------------------------------------------------------------- /examples/flakes/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "bombon": { 4 | "inputs": { 5 | "flake-parts": "flake-parts", 6 | "flake-utils": "flake-utils", 7 | "nixpkgs": [ 8 | "nixpkgs" 9 | ], 10 | "pre-commit-hooks-nix": "pre-commit-hooks-nix", 11 | "systems": "systems" 12 | }, 13 | "locked": { 14 | "lastModified": 1721119620, 15 | "narHash": "sha256-Z0TSvUUxyGCNPqchJaaHCQcT5DlUUUG6YEGoVomc3Mo=", 16 | "owner": "nikstur", 17 | "repo": "bombon", 18 | "rev": "35ce4edf3abf2a6b49f7b72f7dc5f05f700c8cc4", 19 | "type": "github" 20 | }, 21 | "original": { 22 | "owner": "nikstur", 23 | "repo": "bombon", 24 | "type": "github" 25 | } 26 | }, 27 | "flake-compat": { 28 | "flake": false, 29 | "locked": { 30 | "lastModified": 1696426674, 31 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 32 | "owner": "edolstra", 33 | "repo": "flake-compat", 34 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 35 | "type": "github" 36 | }, 37 | "original": { 38 | "owner": "edolstra", 39 | "repo": "flake-compat", 40 | "type": "github" 41 | } 42 | }, 43 | "flake-parts": { 44 | "inputs": { 45 | "nixpkgs-lib": [ 46 | "bombon", 47 | "nixpkgs" 48 | ] 49 | }, 50 | "locked": { 51 | "lastModified": 1719994518, 52 | "narHash": "sha256-pQMhCCHyQGRzdfAkdJ4cIWiw+JNuWsTX7f0ZYSyz0VY=", 53 | "owner": "hercules-ci", 54 | "repo": "flake-parts", 55 | "rev": "9227223f6d922fee3c7b190b2cc238a99527bbb7", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "hercules-ci", 60 | "repo": "flake-parts", 61 | "type": "github" 62 | } 63 | }, 64 | "flake-utils": { 65 | "inputs": { 66 | "systems": [ 67 | "bombon", 68 | "systems" 69 | ] 70 | }, 71 | "locked": { 72 | "lastModified": 1710146030, 73 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 74 | "owner": "numtide", 75 | "repo": "flake-utils", 76 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 77 | "type": "github" 78 | }, 79 | "original": { 80 | "owner": "numtide", 81 | "repo": "flake-utils", 82 | "type": "github" 83 | } 84 | }, 85 | "gitignore": { 86 | "inputs": { 87 | "nixpkgs": [ 88 | "bombon", 89 | "pre-commit-hooks-nix", 90 | "nixpkgs" 91 | ] 92 | }, 93 | "locked": { 94 | "lastModified": 1709087332, 95 | "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 96 | "owner": "hercules-ci", 97 | "repo": "gitignore.nix", 98 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 99 | "type": "github" 100 | }, 101 | "original": { 102 | "owner": "hercules-ci", 103 | "repo": "gitignore.nix", 104 | "type": "github" 105 | } 106 | }, 107 | "nixpkgs": { 108 | "locked": { 109 | "lastModified": 1721080040, 110 | "narHash": "sha256-USDsS90/88RJibP3gEcH1AaVt+JpnX4XCUD9bAJP5I4=", 111 | "owner": "NixOS", 112 | "repo": "nixpkgs", 113 | "rev": "b2c1f10bfbb3f617ea8e8669ac13f3f56ceb2ea2", 114 | "type": "github" 115 | }, 116 | "original": { 117 | "owner": "NixOS", 118 | "ref": "nixpkgs-unstable", 119 | "repo": "nixpkgs", 120 | "type": "github" 121 | } 122 | }, 123 | "nixpkgs-stable": { 124 | "locked": { 125 | "lastModified": 1720386169, 126 | "narHash": "sha256-NGKVY4PjzwAa4upkGtAMz1npHGoRzWotlSnVlqI40mo=", 127 | "owner": "NixOS", 128 | "repo": "nixpkgs", 129 | "rev": "194846768975b7ad2c4988bdb82572c00222c0d7", 130 | "type": "github" 131 | }, 132 | "original": { 133 | "owner": "NixOS", 134 | "ref": "nixos-24.05", 135 | "repo": "nixpkgs", 136 | "type": "github" 137 | } 138 | }, 139 | "pre-commit-hooks-nix": { 140 | "inputs": { 141 | "flake-compat": "flake-compat", 142 | "gitignore": "gitignore", 143 | "nixpkgs": [ 144 | "bombon", 145 | "nixpkgs" 146 | ], 147 | "nixpkgs-stable": "nixpkgs-stable" 148 | }, 149 | "locked": { 150 | "lastModified": 1720524665, 151 | "narHash": "sha256-ni/87oHPZm6Gv0ECYxr1f6uxB0UKBWJ6HvS7lwLU6oY=", 152 | "owner": "cachix", 153 | "repo": "pre-commit-hooks.nix", 154 | "rev": "8d6a17d0cdf411c55f12602624df6368ad86fac1", 155 | "type": "github" 156 | }, 157 | "original": { 158 | "owner": "cachix", 159 | "repo": "pre-commit-hooks.nix", 160 | "type": "github" 161 | } 162 | }, 163 | "root": { 164 | "inputs": { 165 | "bombon": "bombon", 166 | "nixpkgs": "nixpkgs" 167 | } 168 | }, 169 | "systems": { 170 | "locked": { 171 | "lastModified": 1681028828, 172 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 173 | "owner": "nix-systems", 174 | "repo": "default", 175 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 176 | "type": "github" 177 | }, 178 | "original": { 179 | "owner": "nix-systems", 180 | "repo": "default", 181 | "type": "github" 182 | } 183 | } 184 | }, 185 | "root": "root", 186 | "version": 7 187 | } 188 | -------------------------------------------------------------------------------- /examples/flakes/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 | bombon.url = "github:nikstur/bombon"; 5 | bombon.inputs.nixpkgs.follows = "nixpkgs"; 6 | }; 7 | 8 | outputs = 9 | { 10 | self, 11 | nixpkgs, 12 | bombon, 13 | }: 14 | let 15 | system = "x86_64-linux"; 16 | pkgs = import nixpkgs { inherit system; }; 17 | in 18 | { 19 | packages.${system}.default = bombon.lib.${system}.buildBom pkgs.hello { }; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /examples/niv/default.nix: -------------------------------------------------------------------------------- 1 | let 2 | sources = import ./nix/sources.nix { }; 3 | pkgs = import sources.nixpkgs { }; 4 | bombon = import sources.bombon { inherit pkgs; }; 5 | in 6 | bombon.buildBom pkgs.hello { } 7 | -------------------------------------------------------------------------------- /examples/niv/nix/sources.json: -------------------------------------------------------------------------------- 1 | { 2 | "bombon": { 3 | "branch": "main", 4 | "repo": "git@github.com:nikstur/bombon.git", 5 | "rev": "06d01a50b350ccbe5033311f80472d96ec140bdc", 6 | "type": "git" 7 | }, 8 | "nixpkgs": { 9 | "branch": "nixpkgs-unstable", 10 | "description": "Nix Packages collection", 11 | "homepage": "", 12 | "owner": "NixOS", 13 | "repo": "nixpkgs", 14 | "rev": "b2c1f10bfbb3f617ea8e8669ac13f3f56ceb2ea2", 15 | "sha256": "13p49w16rza014bpx7b9wavra1nl0x3q1xxki54w9wrzvm5yq82i", 16 | "type": "tarball", 17 | "url": "https://github.com/NixOS/nixpkgs/archive/b2c1f10bfbb3f617ea8e8669ac13f3f56ceb2ea2.tar.gz", 18 | "url_template": "https://github.com///archive/.tar.gz" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/niv/nix/sources.nix: -------------------------------------------------------------------------------- 1 | # This file has been generated by Niv. 2 | 3 | let 4 | 5 | # 6 | # The fetchers. fetch_ fetches specs of type . 7 | # 8 | 9 | fetch_file = pkgs: name: spec: 10 | let 11 | name' = sanitizeName name + "-src"; 12 | in 13 | if spec.builtin or true then 14 | builtins_fetchurl { inherit (spec) url sha256; name = name'; } 15 | else 16 | pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; 17 | 18 | fetch_tarball = pkgs: name: spec: 19 | let 20 | name' = sanitizeName name + "-src"; 21 | in 22 | if spec.builtin or true then 23 | builtins_fetchTarball { name = name'; inherit (spec) url sha256; } 24 | else 25 | pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; 26 | 27 | fetch_git = name: spec: 28 | let 29 | ref = 30 | if spec ? ref then spec.ref else 31 | if spec ? branch then "refs/heads/${spec.branch}" else 32 | if spec ? tag then "refs/tags/${spec.tag}" else 33 | abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!"; 34 | submodules = if spec ? submodules then spec.submodules else false; 35 | submoduleArg = 36 | let 37 | nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0; 38 | emptyArgWithWarning = 39 | if submodules == true 40 | then 41 | builtins.trace 42 | ( 43 | "The niv input \"${name}\" uses submodules " 44 | + "but your nix's (${builtins.nixVersion}) builtins.fetchGit " 45 | + "does not support them" 46 | ) 47 | { } 48 | else { }; 49 | in 50 | if nixSupportsSubmodules 51 | then { inherit submodules; } 52 | else emptyArgWithWarning; 53 | in 54 | builtins.fetchGit 55 | ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg); 56 | 57 | fetch_local = spec: spec.path; 58 | 59 | fetch_builtin-tarball = name: throw 60 | ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. 61 | $ niv modify ${name} -a type=tarball -a builtin=true''; 62 | 63 | fetch_builtin-url = name: throw 64 | ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. 65 | $ niv modify ${name} -a type=file -a builtin=true''; 66 | 67 | # 68 | # Various helpers 69 | # 70 | 71 | # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 72 | sanitizeName = name: 73 | ( 74 | concatMapStrings (s: if builtins.isList s then "-" else s) 75 | ( 76 | builtins.split "[^[:alnum:]+._?=-]+" 77 | ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) 78 | ) 79 | ); 80 | 81 | # The set of packages used when specs are fetched using non-builtins. 82 | mkPkgs = sources: system: 83 | let 84 | sourcesNixpkgs = 85 | import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; 86 | hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; 87 | hasThisAsNixpkgsPath = == ./.; 88 | in 89 | if builtins.hasAttr "nixpkgs" sources 90 | then sourcesNixpkgs 91 | else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then 92 | import { } 93 | else 94 | abort 95 | '' 96 | Please specify either (through -I or NIX_PATH=nixpkgs=...) or 97 | add a package called "nixpkgs" to your sources.json. 98 | ''; 99 | 100 | # The actual fetching function. 101 | fetch = pkgs: name: spec: 102 | 103 | if ! builtins.hasAttr "type" spec then 104 | abort "ERROR: niv spec ${name} does not have a 'type' attribute" 105 | else if spec.type == "file" then fetch_file pkgs name spec 106 | else if spec.type == "tarball" then fetch_tarball pkgs name spec 107 | else if spec.type == "git" then fetch_git name spec 108 | else if spec.type == "local" then fetch_local spec 109 | else if spec.type == "builtin-tarball" then fetch_builtin-tarball name 110 | else if spec.type == "builtin-url" then fetch_builtin-url name 111 | else 112 | abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; 113 | 114 | # If the environment variable NIV_OVERRIDE_${name} is set, then use 115 | # the path directly as opposed to the fetched source. 116 | replace = name: drv: 117 | let 118 | saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name; 119 | ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; 120 | in 121 | if ersatz == "" then drv else 122 | # this turns the string into an actual Nix path (for both absolute and 123 | # relative paths) 124 | if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; 125 | 126 | # Ports of functions for older nix versions 127 | 128 | # a Nix version of mapAttrs if the built-in doesn't exist 129 | mapAttrs = builtins.mapAttrs or ( 130 | f: set: with builtins; 131 | listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) 132 | ); 133 | 134 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 135 | range = first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1); 136 | 137 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 138 | stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); 139 | 140 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 141 | stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); 142 | concatMapStrings = f: list: concatStrings (map f list); 143 | concatStrings = builtins.concatStringsSep ""; 144 | 145 | # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 146 | optionalAttrs = cond: as: if cond then as else { }; 147 | 148 | # fetchTarball version that is compatible between all the versions of Nix 149 | builtins_fetchTarball = { url, name ? null, sha256 }@attrs: 150 | let 151 | inherit (builtins) lessThan nixVersion fetchTarball; 152 | in 153 | if lessThan nixVersion "1.12" then 154 | fetchTarball ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) 155 | else 156 | fetchTarball attrs; 157 | 158 | # fetchurl version that is compatible between all the versions of Nix 159 | builtins_fetchurl = { url, name ? null, sha256 }@attrs: 160 | let 161 | inherit (builtins) lessThan nixVersion fetchurl; 162 | in 163 | if lessThan nixVersion "1.12" then 164 | fetchurl ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) 165 | else 166 | fetchurl attrs; 167 | 168 | # Create the final "sources" from the config 169 | mkSources = config: 170 | mapAttrs 171 | ( 172 | name: spec: 173 | if builtins.hasAttr "outPath" spec 174 | then 175 | abort 176 | "The values in sources.json should not have an 'outPath' attribute" 177 | else 178 | spec // { outPath = replace name (fetch config.pkgs name spec); } 179 | ) 180 | config.sources; 181 | 182 | # The "config" used by the fetchers 183 | mkConfig = 184 | { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null 185 | , sources ? if isNull sourcesFile then { } else builtins.fromJSON (builtins.readFile sourcesFile) 186 | , system ? builtins.currentSystem 187 | , pkgs ? mkPkgs sources system 188 | }: rec { 189 | # The sources, i.e. the attribute set of spec name to spec 190 | inherit sources; 191 | 192 | # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers 193 | inherit pkgs; 194 | }; 195 | 196 | in 197 | mkSources (mkConfig { }) // { __functor = _: settings: mkSources (mkConfig settings); } 198 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1696426674, 7 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-parts": { 20 | "inputs": { 21 | "nixpkgs-lib": [ 22 | "nixpkgs" 23 | ] 24 | }, 25 | "locked": { 26 | "lastModified": 1743550720, 27 | "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", 28 | "owner": "hercules-ci", 29 | "repo": "flake-parts", 30 | "rev": "c621e8422220273271f52058f618c94e405bb0f5", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "hercules-ci", 35 | "repo": "flake-parts", 36 | "type": "github" 37 | } 38 | }, 39 | "gitignore": { 40 | "inputs": { 41 | "nixpkgs": [ 42 | "pre-commit-hooks-nix", 43 | "nixpkgs" 44 | ] 45 | }, 46 | "locked": { 47 | "lastModified": 1709087332, 48 | "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 49 | "owner": "hercules-ci", 50 | "repo": "gitignore.nix", 51 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 52 | "type": "github" 53 | }, 54 | "original": { 55 | "owner": "hercules-ci", 56 | "repo": "gitignore.nix", 57 | "type": "github" 58 | } 59 | }, 60 | "nixpkgs": { 61 | "locked": { 62 | "lastModified": 1748693115, 63 | "narHash": "sha256-StSrWhklmDuXT93yc3GrTlb0cKSS0agTAxMGjLKAsY8=", 64 | "owner": "NixOS", 65 | "repo": "nixpkgs", 66 | "rev": "910796cabe436259a29a72e8d3f5e180fc6dfacc", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "NixOS", 71 | "ref": "nixos-unstable", 72 | "repo": "nixpkgs", 73 | "type": "github" 74 | } 75 | }, 76 | "pre-commit-hooks-nix": { 77 | "inputs": { 78 | "flake-compat": "flake-compat", 79 | "gitignore": "gitignore", 80 | "nixpkgs": [ 81 | "nixpkgs" 82 | ] 83 | }, 84 | "locked": { 85 | "lastModified": 1747372754, 86 | "narHash": "sha256-2Y53NGIX2vxfie1rOW0Qb86vjRZ7ngizoo+bnXU9D9k=", 87 | "owner": "cachix", 88 | "repo": "pre-commit-hooks.nix", 89 | "rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46", 90 | "type": "github" 91 | }, 92 | "original": { 93 | "owner": "cachix", 94 | "repo": "pre-commit-hooks.nix", 95 | "type": "github" 96 | } 97 | }, 98 | "root": { 99 | "inputs": { 100 | "flake-parts": "flake-parts", 101 | "nixpkgs": "nixpkgs", 102 | "pre-commit-hooks-nix": "pre-commit-hooks-nix", 103 | "systems": "systems" 104 | } 105 | }, 106 | "systems": { 107 | "locked": { 108 | "lastModified": 1681028828, 109 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 110 | "owner": "nix-systems", 111 | "repo": "default", 112 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 113 | "type": "github" 114 | }, 115 | "original": { 116 | "owner": "nix-systems", 117 | "repo": "default", 118 | "type": "github" 119 | } 120 | } 121 | }, 122 | "root": "root", 123 | "version": 7 124 | } 125 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Nix CycloneDX Software Bills of Materials (SBOMs)"; 3 | 4 | inputs = { 5 | 6 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 7 | 8 | systems.url = "github:nix-systems/default"; 9 | 10 | flake-parts = { 11 | url = "github:hercules-ci/flake-parts"; 12 | inputs.nixpkgs-lib.follows = "nixpkgs"; 13 | }; 14 | 15 | pre-commit-hooks-nix = { 16 | url = "github:cachix/pre-commit-hooks.nix"; 17 | inputs = { 18 | nixpkgs.follows = "nixpkgs"; 19 | }; 20 | }; 21 | 22 | }; 23 | 24 | outputs = 25 | inputs@{ 26 | self, 27 | flake-parts, 28 | systems, 29 | ... 30 | }: 31 | flake-parts.lib.mkFlake { inherit inputs; } { 32 | systems = import systems; 33 | 34 | imports = 35 | let 36 | # This is effectively just boilerplate to allow us to keep the `lib` 37 | # output. 38 | libOutputModule = 39 | { lib, ... }: 40 | flake-parts.lib.mkTransposedPerSystemModule { 41 | name = "lib"; 42 | option = lib.mkOption { 43 | type = lib.types.lazyAttrsOf lib.types.anything; 44 | default = { }; 45 | }; 46 | file = ""; 47 | }; 48 | in 49 | [ 50 | inputs.pre-commit-hooks-nix.flakeModule 51 | libOutputModule 52 | ]; 53 | 54 | flake = { 55 | templates.default = { 56 | path = builtins.filterSource (path: type: baseNameOf path == "flake.nix") ./examples/flakes; 57 | description = "Build a Bom for GNU hello"; 58 | }; 59 | }; 60 | 61 | perSystem = 62 | { 63 | config, 64 | system, 65 | pkgs, 66 | lib, 67 | ... 68 | }: 69 | let 70 | bombon = import ./. { inherit pkgs; }; 71 | inherit (bombon) transformer buildBom passthruVendoredSbom; 72 | in 73 | { 74 | lib = { 75 | inherit buildBom passthruVendoredSbom; 76 | }; 77 | 78 | packages = { 79 | # This is mostly here for development 80 | inherit transformer; 81 | default = transformer; 82 | sbom = buildBom transformer { }; 83 | }; 84 | 85 | checks = transformer.tests // import ./nix/tests { inherit pkgs buildBom passthruVendoredSbom; }; 86 | 87 | pre-commit = { 88 | check.enable = true; 89 | 90 | settings = { 91 | hooks = { 92 | nixfmt-rfc-style = { 93 | enable = true; 94 | excludes = [ "sources.nix" ]; 95 | }; 96 | typos.enable = true; 97 | statix = { 98 | enable = true; 99 | settings.ignore = [ "sources.nix" ]; 100 | }; 101 | }; 102 | }; 103 | }; 104 | 105 | devShells.default = pkgs.mkShell { 106 | shellHook = '' 107 | ${config.pre-commit.installationScript} 108 | ''; 109 | 110 | packages = [ 111 | pkgs.niv 112 | pkgs.clippy 113 | pkgs.rustfmt 114 | pkgs.cargo-machete 115 | pkgs.cargo-edit 116 | pkgs.cargo-bloat 117 | pkgs.cargo-deny 118 | pkgs.cargo-cyclonedx 119 | ]; 120 | 121 | inputsFrom = [ transformer ]; 122 | 123 | RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; 124 | }; 125 | 126 | }; 127 | }; 128 | } 129 | -------------------------------------------------------------------------------- /nix/build-bom.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | runCommand, 4 | transformer, 5 | buildtimeDependencies, 6 | runtimeDependencies, 7 | }: 8 | 9 | drv: 10 | { 11 | extraPaths ? [ ], 12 | includeBuildtimeDependencies ? false, 13 | excludes ? [ ], 14 | }: 15 | 16 | let 17 | args = 18 | lib.optionals includeBuildtimeDependencies [ 19 | "--include-buildtime-dependencies" 20 | ] 21 | ++ lib.optionals (excludes != [ ]) (lib.map (e: "--exclude ${e}") excludes); 22 | in 23 | runCommand "${drv.name}.cdx.json" { nativeBuildInputs = [ transformer ]; } '' 24 | bombon-transformer ${drv} \ 25 | ${toString args} \ 26 | ${buildtimeDependencies drv extraPaths} \ 27 | ${runtimeDependencies drv extraPaths} \ 28 | $out 29 | '' 30 | -------------------------------------------------------------------------------- /nix/buildtime-dependencies.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | writeText, 4 | runCommand, 5 | jq, 6 | }: 7 | 8 | let 9 | 10 | # Find the outputs of a derivation. 11 | # 12 | # Returns a list of all derivations that correspond to an output of the input 13 | # derivation. 14 | drvOutputs = 15 | drv: if builtins.hasAttr "outputs" drv then map (output: drv.${output}) drv.outputs else [ drv ]; 16 | 17 | # Find the dependencies of a derivation via it's `drvAttrs`. 18 | # 19 | # Returns a list of all dependencies. 20 | drvDeps = 21 | drv: 22 | lib.mapAttrsToList ( 23 | k: v: 24 | if lib.isDerivation v then 25 | (drvOutputs v) 26 | else if lib.isList v then 27 | lib.concatMap drvOutputs (lib.filter lib.isDerivation v) 28 | else 29 | [ ] 30 | ) drv.drvAttrs; 31 | 32 | wrap = drv: { 33 | key = drv.outPath; 34 | inherit drv; 35 | }; 36 | 37 | # Walk through the whole DAG of dependencies, using the `outPath` as an 38 | # index for the elements. 39 | # 40 | # Returns a list of all of `drv`'s buildtime dependencies. 41 | # Elements in the list have two fields: 42 | # 43 | # - key: the store path of the input. 44 | # - drv: the actual derivation object. 45 | # 46 | # All outputs are included because they have different outPaths 47 | buildtimeDerivations = 48 | drv0: 49 | builtins.genericClosure { 50 | startSet = map wrap (drvOutputs drv0); 51 | operator = item: map wrap (lib.concatLists (drvDeps item.drv)); 52 | }; 53 | 54 | # Like lib.getAttrs but omit attrs that do not exist. 55 | optionalGetAttrs = 56 | names: attrs: lib.genAttrs (builtins.filter (x: lib.hasAttr x attrs) names) (name: attrs.${name}); 57 | 58 | # Retrieve only the required fields from a derivation. 59 | # 60 | # Also renames outPath so that builtins.toJSON actually emits JSON and not 61 | # only the nix store path. 62 | fields = 63 | drv: 64 | (optionalGetAttrs [ 65 | "name" 66 | "pname" 67 | "version" 68 | "meta" 69 | "outputName" 70 | "outputHash" 71 | ] drv) 72 | // { 73 | path = drv.outPath; 74 | patches = lib.flatten (drv.patches or [ ]); 75 | } 76 | // lib.optionalAttrs (drv ? src && drv.src ? urls) { 77 | src = 78 | { 79 | inherit (drv.src) urls; 80 | } 81 | // lib.optionalAttrs (drv.src ? outputHash) { 82 | hash = drv.src.outputHash; 83 | }; 84 | } 85 | // lib.optionalAttrs (drv ? bombonVendoredSbom) { 86 | vendoredSbom = drv.bombonVendoredSbom.outPath; 87 | }; 88 | 89 | in 90 | 91 | drv: extraPaths: 92 | 93 | let 94 | 95 | allDrvs = [ drv ] ++ extraPaths; 96 | 97 | allBuildtimeDerivations = lib.flatten (map buildtimeDerivations allDrvs); 98 | 99 | unformattedJson = writeText "${drv.name}-unformatted-buildtime-dependencies.json" ( 100 | builtins.toJSON (map (item: (fields item.drv)) allBuildtimeDerivations) 101 | ); 102 | 103 | in 104 | 105 | # Format the json so that the transformer can better report where errors occur 106 | runCommand "${drv.name}-buildtime-dependencies.json" { } '' 107 | ${jq}/bin/jq < ${unformattedJson} > "$out" 108 | '' 109 | -------------------------------------------------------------------------------- /nix/packages/transformer.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | rustPlatform, 4 | clippy, 5 | rustfmt, 6 | }: 7 | 8 | let 9 | cargoToml = builtins.fromTOML (builtins.readFile ../../rust/transformer/Cargo.toml); 10 | in 11 | rustPlatform.buildRustPackage (finalAttrs: { 12 | pname = cargoToml.package.name; 13 | inherit (cargoToml.package) version; 14 | 15 | src = lib.sourceFilesBySuffices ../../rust/transformer [ 16 | ".rs" 17 | ".toml" 18 | ".lock" 19 | ]; 20 | 21 | cargoLock = { 22 | lockFile = ../../rust/transformer/Cargo.lock; 23 | }; 24 | 25 | passthru.tests = { 26 | clippy = finalAttrs.finalPackage.overrideAttrs ( 27 | _: previousAttrs: { 28 | pname = previousAttrs.pname + "-clippy"; 29 | nativeCheckInputs = (previousAttrs.nativeCheckInputs or [ ]) ++ [ clippy ]; 30 | checkPhase = "cargo clippy"; 31 | } 32 | ); 33 | rustfmt = finalAttrs.finalPackage.overrideAttrs ( 34 | _: previousAttrs: { 35 | pname = previousAttrs.pname + "-rustfmt"; 36 | nativeCheckInputs = (previousAttrs.nativeCheckInputs or [ ]) ++ [ rustfmt ]; 37 | checkPhase = "cargo fmt --check"; 38 | } 39 | ); 40 | }; 41 | 42 | meta = with lib; { 43 | homepage = "https://github.com/nikstur/bombon"; 44 | license = licenses.mit; 45 | maintainers = with lib.maintainers; [ nikstur ]; 46 | mainProgram = "bombon-transformer"; 47 | }; 48 | }) 49 | -------------------------------------------------------------------------------- /nix/passthru-vendored.nix: -------------------------------------------------------------------------------- 1 | { 2 | 3 | # Add a passthru derivation to a Rust derivation `package` that generates a 4 | # CycloneDX SBOM. 5 | # 6 | # This could be done much more elegantly if `buildRustPackage` supported 7 | # finalAttrs. When https://github.com/NixOS/nixpkgs/pull/194475 lands, we can 8 | # most likely get rid of this. 9 | rust = 10 | package: 11 | { 12 | pkgs, 13 | includeBuildtimeDependencies ? false, 14 | }: 15 | package.overrideAttrs (previousAttrs: { 16 | passthru = (previousAttrs.passthru or { }) // { 17 | bombonVendoredSbom = package.overrideAttrs (previousAttrs: { 18 | pname = previousAttrs.pname + "-bombon-vendored-sbom"; 19 | nativeBuildInputs = (previousAttrs.nativeBuildInputs or [ ]) ++ [ 20 | pkgs.buildPackages.cargo-cyclonedx 21 | ]; 22 | outputs = [ "out" ]; 23 | phases = [ 24 | "unpackPhase" 25 | "patchPhase" 26 | "configurePhase" 27 | "buildPhase" 28 | "installPhase" 29 | ]; 30 | 31 | buildPhase = 32 | '' 33 | cargo cyclonedx \ 34 | --spec-version 1.5 \ 35 | --format json \ 36 | --describe binaries \ 37 | --target ${pkgs.stdenv.hostPlatform.rust.rustcTarget} \ 38 | '' 39 | + pkgs.lib.optionalString ( 40 | builtins.hasAttr "buildNoDefaultFeatures" previousAttrs && previousAttrs.buildNoDefaultFeatures 41 | ) " --no-default-features" 42 | + pkgs.lib.optionalString ( 43 | builtins.hasAttr "buildFeatures" previousAttrs && builtins.length previousAttrs.buildFeatures > 0 44 | ) (" --features " + builtins.concatStringsSep "," previousAttrs.buildFeatures) 45 | + pkgs.lib.optionalString (!includeBuildtimeDependencies) " --no-build-deps"; 46 | 47 | installPhase = '' 48 | mkdir -p $out 49 | 50 | # Collect all paths to executable files. Cargo has no good support to find this 51 | # and this method is very robust. The flipside is that we have to build the package 52 | # to generate a BOM for it. 53 | mapfile -d "" binaries < <(find ${package} -type f -executable -print0) 54 | 55 | for binary in "''${binaries[@]}"; do 56 | base=$(basename $binary) 57 | 58 | # Strip binary suffixes 59 | base=''${base%.exe} 60 | base=''${base%.efi} 61 | 62 | cdx=$(find . -name "''${base}_bin.cdx.json") 63 | 64 | if [ -f "$cdx" ]; then 65 | echo "Found SBOM for binary '$binary': $cdx" 66 | install -m444 "$cdx" $out/ 67 | else 68 | echo "Failed to find SBOM for binary: $binary" 69 | exit 1 70 | fi 71 | done 72 | ''; 73 | 74 | separateDebugInfo = false; 75 | }); 76 | }; 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /nix/runtime-dependencies.nix: -------------------------------------------------------------------------------- 1 | # This is a wrapper around nixpkgs' closureInfo. It returns a newline 2 | # separated list of the store paths of drv's runtime dependencies. 3 | { 4 | runCommand, 5 | closureInfo, 6 | }: 7 | 8 | drv: extraPaths: 9 | runCommand "${drv.name}-runtime-dependencies.txt" { } '' 10 | cat ${closureInfo { rootPaths = [ drv ] ++ extraPaths; }}/store-paths > $out 11 | '' 12 | -------------------------------------------------------------------------------- /nix/tests/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs, 3 | buildBom, 4 | passthruVendoredSbom, 5 | }: 6 | 7 | let 8 | rustPassthru = pkg: passthruVendoredSbom.rust pkg { inherit pkgs; }; 9 | 10 | buildtimeOptions = { 11 | includeBuildtimeDependencies = true; 12 | }; 13 | 14 | # This list cannot grow indefinitely because building a Bom requires all 15 | # builtime dependencies to be downloaded or built. A lot of time is spent 16 | # evaluating, downloading, and building. 17 | testDerivations = with pkgs; [ 18 | { 19 | name = "hello"; 20 | drv = hello; 21 | options = { }; 22 | } 23 | { 24 | name = "hello-buildtime"; 25 | drv = hello; 26 | options = buildtimeOptions; 27 | } 28 | 29 | { 30 | name = "python3"; 31 | drv = python3; 32 | options = { }; 33 | } 34 | { 35 | name = "python3-buildtime"; 36 | drv = python3; 37 | options = buildtimeOptions; 38 | } 39 | 40 | # weird string license in buildtimeDependencies 41 | { 42 | name = "poetry"; 43 | drv = poetry; 44 | options = { }; 45 | } 46 | { 47 | name = "poetry-buildtime"; 48 | drv = poetry; 49 | options = buildtimeOptions; 50 | } 51 | 52 | { 53 | name = "git"; 54 | drv = git; 55 | options = { }; 56 | } 57 | { 58 | name = "git-buildtime"; 59 | drv = git; 60 | options = buildtimeOptions; 61 | } 62 | 63 | { 64 | name = "git-extra-paths"; 65 | drv = git; 66 | options = { 67 | extraPaths = [ poetry ]; 68 | }; 69 | } 70 | { 71 | name = "git-extra-paths-buildtime"; 72 | drv = git; 73 | options = buildtimeOptions // { 74 | extraPaths = [ poetry ]; 75 | }; 76 | } 77 | 78 | { 79 | name = "cloud-hypervisor"; 80 | drv = rustPassthru cloud-hypervisor; 81 | options = { }; 82 | } 83 | { 84 | name = "cloud-hypervisor"; 85 | drv = rustPassthru cloud-hypervisor; 86 | options = buildtimeOptions; 87 | } 88 | 89 | # Multiple src urls 90 | { 91 | name = "kexec-tools"; 92 | drv = kexec-tools; 93 | options = { }; 94 | } 95 | ]; 96 | 97 | cycloneDxVersion = "1.5"; 98 | 99 | cycloneDxSpec = pkgs.fetchFromGitHub { 100 | owner = "CycloneDX"; 101 | repo = "specification"; 102 | rev = cycloneDxVersion; 103 | sha256 = "sha256-bAXi7m7kWJ+lZNYnvSNmLQ6+kLqRw379crbC3viNqzY="; 104 | }; 105 | 106 | buildBomAndValidate = 107 | drv: options: 108 | pkgs.runCommand "${drv.name}-bom-validation" { nativeBuildInputs = [ pkgs.check-jsonschema ]; } '' 109 | sbom="${buildBom drv options}" 110 | check-jsonschema \ 111 | --schemafile "${cycloneDxSpec}/schema/bom-${cycloneDxVersion}.schema.json" \ 112 | --base-uri "${cycloneDxSpec}/schema/bom-${cycloneDxVersion}.schema.json" \ 113 | "$sbom" 114 | ln -s $sbom $out 115 | ''; 116 | 117 | genAttrsFromDrvs = 118 | drvs: f: builtins.listToAttrs (map (d: pkgs.lib.nameValuePair d.name (f d.drv d.options)) drvs); 119 | in 120 | genAttrsFromDrvs testDerivations buildBomAndValidate 121 | -------------------------------------------------------------------------------- /rust/transformer/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstyle" 16 | version = "1.0.10" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 19 | 20 | [[package]] 21 | name = "anyhow" 22 | version = "1.0.98" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 25 | 26 | [[package]] 27 | name = "autocfg" 28 | version = "1.4.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 31 | 32 | [[package]] 33 | name = "base64" 34 | version = "0.21.7" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 37 | 38 | [[package]] 39 | name = "base64" 40 | version = "0.22.1" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 43 | 44 | [[package]] 45 | name = "bitflags" 46 | version = "1.3.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 49 | 50 | [[package]] 51 | name = "bitflags" 52 | version = "2.9.1" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 55 | 56 | [[package]] 57 | name = "block-buffer" 58 | version = "0.10.4" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 61 | dependencies = [ 62 | "generic-array", 63 | ] 64 | 65 | [[package]] 66 | name = "bombon-transformer" 67 | version = "0.3.0" 68 | dependencies = [ 69 | "anyhow", 70 | "base64 0.22.1", 71 | "clap", 72 | "cyclonedx-bom", 73 | "itertools", 74 | "regex", 75 | "serde", 76 | "serde_json", 77 | "sha2", 78 | "uuid", 79 | ] 80 | 81 | [[package]] 82 | name = "bumpalo" 83 | version = "3.17.0" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 86 | 87 | [[package]] 88 | name = "cfg-if" 89 | version = "1.0.0" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 92 | 93 | [[package]] 94 | name = "clap" 95 | version = "4.5.39" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" 98 | dependencies = [ 99 | "clap_builder", 100 | "clap_derive", 101 | ] 102 | 103 | [[package]] 104 | name = "clap_builder" 105 | version = "4.5.39" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" 108 | dependencies = [ 109 | "anstyle", 110 | "clap_lex", 111 | ] 112 | 113 | [[package]] 114 | name = "clap_derive" 115 | version = "4.5.32" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 118 | dependencies = [ 119 | "heck", 120 | "proc-macro2", 121 | "quote", 122 | "syn", 123 | ] 124 | 125 | [[package]] 126 | name = "clap_lex" 127 | version = "0.7.4" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 130 | 131 | [[package]] 132 | name = "cpufeatures" 133 | version = "0.2.17" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 136 | dependencies = [ 137 | "libc", 138 | ] 139 | 140 | [[package]] 141 | name = "crypto-common" 142 | version = "0.1.6" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 145 | dependencies = [ 146 | "generic-array", 147 | "typenum", 148 | ] 149 | 150 | [[package]] 151 | name = "cyclonedx-bom" 152 | version = "0.8.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "ce2ec98a191e17f63b92b132f6852462de9eaee03ca8dbf2df401b9fd809bcac" 155 | dependencies = [ 156 | "base64 0.21.7", 157 | "cyclonedx-bom-macros", 158 | "fluent-uri", 159 | "indexmap", 160 | "once_cell", 161 | "ordered-float", 162 | "purl", 163 | "regex", 164 | "serde", 165 | "serde_json", 166 | "spdx", 167 | "strum", 168 | "thiserror", 169 | "time", 170 | "uuid", 171 | "xml-rs", 172 | ] 173 | 174 | [[package]] 175 | name = "cyclonedx-bom-macros" 176 | version = "0.1.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "c50341f21df64b412b4f917e34b7aa786c092d64f3f905f478cb76950c7e980c" 179 | dependencies = [ 180 | "proc-macro2", 181 | "quote", 182 | "syn", 183 | ] 184 | 185 | [[package]] 186 | name = "deranged" 187 | version = "0.3.11" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 190 | dependencies = [ 191 | "powerfmt", 192 | ] 193 | 194 | [[package]] 195 | name = "digest" 196 | version = "0.10.7" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 199 | dependencies = [ 200 | "block-buffer", 201 | "crypto-common", 202 | ] 203 | 204 | [[package]] 205 | name = "either" 206 | version = "1.13.0" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 209 | 210 | [[package]] 211 | name = "equivalent" 212 | version = "1.0.1" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 215 | 216 | [[package]] 217 | name = "fluent-uri" 218 | version = "0.1.4" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" 221 | dependencies = [ 222 | "bitflags 1.3.2", 223 | ] 224 | 225 | [[package]] 226 | name = "generic-array" 227 | version = "0.14.7" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 230 | dependencies = [ 231 | "typenum", 232 | "version_check", 233 | ] 234 | 235 | [[package]] 236 | name = "getrandom" 237 | version = "0.3.3" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 240 | dependencies = [ 241 | "cfg-if", 242 | "libc", 243 | "r-efi", 244 | "wasi", 245 | ] 246 | 247 | [[package]] 248 | name = "hashbrown" 249 | version = "0.15.2" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 252 | 253 | [[package]] 254 | name = "heck" 255 | version = "0.5.0" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 258 | 259 | [[package]] 260 | name = "hex" 261 | version = "0.4.3" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 264 | 265 | [[package]] 266 | name = "indexmap" 267 | version = "2.6.0" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" 270 | dependencies = [ 271 | "equivalent", 272 | "hashbrown", 273 | ] 274 | 275 | [[package]] 276 | name = "itertools" 277 | version = "0.14.0" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 280 | dependencies = [ 281 | "either", 282 | ] 283 | 284 | [[package]] 285 | name = "itoa" 286 | version = "1.0.15" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 289 | 290 | [[package]] 291 | name = "js-sys" 292 | version = "0.3.77" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 295 | dependencies = [ 296 | "once_cell", 297 | "wasm-bindgen", 298 | ] 299 | 300 | [[package]] 301 | name = "libc" 302 | version = "0.2.172" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 305 | 306 | [[package]] 307 | name = "log" 308 | version = "0.4.27" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 311 | 312 | [[package]] 313 | name = "memchr" 314 | version = "2.7.4" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 317 | 318 | [[package]] 319 | name = "num-conv" 320 | version = "0.1.0" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 323 | 324 | [[package]] 325 | name = "num-traits" 326 | version = "0.2.19" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 329 | dependencies = [ 330 | "autocfg", 331 | ] 332 | 333 | [[package]] 334 | name = "once_cell" 335 | version = "1.21.3" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 338 | 339 | [[package]] 340 | name = "ordered-float" 341 | version = "4.5.0" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "c65ee1f9701bf938026630b455d5315f490640234259037edb259798b3bcf85e" 344 | dependencies = [ 345 | "num-traits", 346 | ] 347 | 348 | [[package]] 349 | name = "percent-encoding" 350 | version = "2.3.1" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 353 | 354 | [[package]] 355 | name = "powerfmt" 356 | version = "0.2.0" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 359 | 360 | [[package]] 361 | name = "proc-macro2" 362 | version = "1.0.95" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 365 | dependencies = [ 366 | "unicode-ident", 367 | ] 368 | 369 | [[package]] 370 | name = "purl" 371 | version = "0.1.3" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "c14fe28c8495f7eaf77a6e6106966f95211c0a2404b9da50d248fc32af3a3f14" 374 | dependencies = [ 375 | "hex", 376 | "percent-encoding", 377 | "thiserror", 378 | ] 379 | 380 | [[package]] 381 | name = "quote" 382 | version = "1.0.40" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 385 | dependencies = [ 386 | "proc-macro2", 387 | ] 388 | 389 | [[package]] 390 | name = "r-efi" 391 | version = "5.2.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 394 | 395 | [[package]] 396 | name = "regex" 397 | version = "1.11.1" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 400 | dependencies = [ 401 | "aho-corasick", 402 | "memchr", 403 | "regex-automata", 404 | "regex-syntax", 405 | ] 406 | 407 | [[package]] 408 | name = "regex-automata" 409 | version = "0.4.9" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 412 | dependencies = [ 413 | "aho-corasick", 414 | "memchr", 415 | "regex-syntax", 416 | ] 417 | 418 | [[package]] 419 | name = "regex-syntax" 420 | version = "0.8.5" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 423 | 424 | [[package]] 425 | name = "rustversion" 426 | version = "1.0.20" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 429 | 430 | [[package]] 431 | name = "ryu" 432 | version = "1.0.20" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 435 | 436 | [[package]] 437 | name = "serde" 438 | version = "1.0.219" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 441 | dependencies = [ 442 | "serde_derive", 443 | ] 444 | 445 | [[package]] 446 | name = "serde_derive" 447 | version = "1.0.219" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 450 | dependencies = [ 451 | "proc-macro2", 452 | "quote", 453 | "syn", 454 | ] 455 | 456 | [[package]] 457 | name = "serde_json" 458 | version = "1.0.140" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 461 | dependencies = [ 462 | "itoa", 463 | "memchr", 464 | "ryu", 465 | "serde", 466 | ] 467 | 468 | [[package]] 469 | name = "sha2" 470 | version = "0.10.9" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 473 | dependencies = [ 474 | "cfg-if", 475 | "cpufeatures", 476 | "digest", 477 | ] 478 | 479 | [[package]] 480 | name = "smallvec" 481 | version = "1.13.2" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 484 | 485 | [[package]] 486 | name = "spdx" 487 | version = "0.10.7" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "bae30cc7bfe3656d60ee99bf6836f472b0c53dddcbf335e253329abb16e535a2" 490 | dependencies = [ 491 | "smallvec", 492 | ] 493 | 494 | [[package]] 495 | name = "strum" 496 | version = "0.26.3" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 499 | dependencies = [ 500 | "strum_macros", 501 | ] 502 | 503 | [[package]] 504 | name = "strum_macros" 505 | version = "0.26.4" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 508 | dependencies = [ 509 | "heck", 510 | "proc-macro2", 511 | "quote", 512 | "rustversion", 513 | "syn", 514 | ] 515 | 516 | [[package]] 517 | name = "syn" 518 | version = "2.0.100" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 521 | dependencies = [ 522 | "proc-macro2", 523 | "quote", 524 | "unicode-ident", 525 | ] 526 | 527 | [[package]] 528 | name = "thiserror" 529 | version = "1.0.69" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 532 | dependencies = [ 533 | "thiserror-impl", 534 | ] 535 | 536 | [[package]] 537 | name = "thiserror-impl" 538 | version = "1.0.69" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 541 | dependencies = [ 542 | "proc-macro2", 543 | "quote", 544 | "syn", 545 | ] 546 | 547 | [[package]] 548 | name = "time" 549 | version = "0.3.36" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 552 | dependencies = [ 553 | "deranged", 554 | "itoa", 555 | "num-conv", 556 | "powerfmt", 557 | "serde", 558 | "time-core", 559 | "time-macros", 560 | ] 561 | 562 | [[package]] 563 | name = "time-core" 564 | version = "0.1.2" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 567 | 568 | [[package]] 569 | name = "time-macros" 570 | version = "0.2.18" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 573 | dependencies = [ 574 | "num-conv", 575 | "time-core", 576 | ] 577 | 578 | [[package]] 579 | name = "typenum" 580 | version = "1.18.0" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 583 | 584 | [[package]] 585 | name = "unicode-ident" 586 | version = "1.0.18" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 589 | 590 | [[package]] 591 | name = "uuid" 592 | version = "1.17.0" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" 595 | dependencies = [ 596 | "getrandom", 597 | "js-sys", 598 | "wasm-bindgen", 599 | ] 600 | 601 | [[package]] 602 | name = "version_check" 603 | version = "0.9.5" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 606 | 607 | [[package]] 608 | name = "wasi" 609 | version = "0.14.2+wasi-0.2.4" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 612 | dependencies = [ 613 | "wit-bindgen-rt", 614 | ] 615 | 616 | [[package]] 617 | name = "wasm-bindgen" 618 | version = "0.2.100" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 621 | dependencies = [ 622 | "cfg-if", 623 | "once_cell", 624 | "rustversion", 625 | "wasm-bindgen-macro", 626 | ] 627 | 628 | [[package]] 629 | name = "wasm-bindgen-backend" 630 | version = "0.2.100" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 633 | dependencies = [ 634 | "bumpalo", 635 | "log", 636 | "proc-macro2", 637 | "quote", 638 | "syn", 639 | "wasm-bindgen-shared", 640 | ] 641 | 642 | [[package]] 643 | name = "wasm-bindgen-macro" 644 | version = "0.2.100" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 647 | dependencies = [ 648 | "quote", 649 | "wasm-bindgen-macro-support", 650 | ] 651 | 652 | [[package]] 653 | name = "wasm-bindgen-macro-support" 654 | version = "0.2.100" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 657 | dependencies = [ 658 | "proc-macro2", 659 | "quote", 660 | "syn", 661 | "wasm-bindgen-backend", 662 | "wasm-bindgen-shared", 663 | ] 664 | 665 | [[package]] 666 | name = "wasm-bindgen-shared" 667 | version = "0.2.100" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 670 | dependencies = [ 671 | "unicode-ident", 672 | ] 673 | 674 | [[package]] 675 | name = "wit-bindgen-rt" 676 | version = "0.39.0" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 679 | dependencies = [ 680 | "bitflags 2.9.1", 681 | ] 682 | 683 | [[package]] 684 | name = "xml-rs" 685 | version = "0.8.23" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f" 688 | -------------------------------------------------------------------------------- /rust/transformer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bombon-transformer" 3 | version = "0.3.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1.0.98" 8 | clap = { version = "4.5.39", default-features = false, features = ["std", "derive"] } 9 | cyclonedx-bom = "0.8.0" 10 | itertools = "0.14.0" 11 | serde = { version = "1.0.219", features = [ "derive" ] } 12 | serde_json = "1.0.140" 13 | sha2 = "0.10.9" 14 | uuid = "1.17.0" 15 | base64 = "0.22.1" 16 | regex = "1.11.1" 17 | 18 | [lints.rust] 19 | unsafe_code = "forbid" 20 | 21 | [lints.clippy] 22 | all = { level = "deny" } 23 | pedantic = { level = "deny" } 24 | unwrap_used = { level = "deny" } 25 | expect_used = { level = "deny" } 26 | lint_groups_priority = { level = "allow", priority = 1 } # https://github.com/rust-lang/rust-clippy/issues/12270 27 | -------------------------------------------------------------------------------- /rust/transformer/src/buildtime_input.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::fs; 3 | use std::path::Path; 4 | 5 | use anyhow::{Context, Result}; 6 | 7 | use crate::derivation::Derivation; 8 | 9 | #[derive(Clone)] 10 | pub struct BuildtimeInput(pub BTreeMap); 11 | 12 | impl BuildtimeInput { 13 | pub fn from_file(path: &Path) -> Result { 14 | let buildtime_input_json: Vec = serde_json::from_reader( 15 | fs::File::open(path).with_context(|| format!("Failed to open {path:?}"))?, 16 | ) 17 | .with_context(|| format!("Failed to parse buildtime input at {path:?}"))?; 18 | let mut m = BTreeMap::new(); 19 | for derivation in buildtime_input_json { 20 | m.insert(derivation.path.clone(), derivation); 21 | } 22 | Ok(Self(m)) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /rust/transformer/src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::Result; 4 | use clap::Parser; 5 | 6 | use crate::transform::transform; 7 | 8 | #[derive(Parser)] 9 | pub struct Cli { 10 | /// Include buildtime dependencies in output 11 | #[arg(long)] 12 | include_buildtime_dependencies: bool, 13 | 14 | /// Regex pattern of store paths to exclude from the final SBOM. 15 | /// 16 | /// Can be given multiple times to exclude multiple patterns. 17 | #[arg(short, long)] 18 | exclude: Vec, 19 | 20 | /// Path to target derivation 21 | target: String, 22 | 23 | /// Path to JSON containing the buildtime input 24 | buildtime_input: PathBuf, 25 | 26 | /// Path to a newline separated .txt file containing the runtime input 27 | runtime_input: PathBuf, 28 | 29 | /// Path to write the SBOM to 30 | output: PathBuf, 31 | } 32 | 33 | impl Cli { 34 | pub fn call(self) -> Result<()> { 35 | transform( 36 | self.include_buildtime_dependencies, 37 | &self.exclude, 38 | &self.target, 39 | &self.buildtime_input, 40 | &self.runtime_input, 41 | &self.output, 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rust/transformer/src/cyclonedx.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::convert::Into; 3 | use std::fs; 4 | use std::path::Path; 5 | use std::str::FromStr; 6 | 7 | use anyhow::{Context, Result}; 8 | use cyclonedx_bom::external_models::normalized_string::NormalizedString; 9 | use cyclonedx_bom::external_models::uri::{Purl, Uri}; 10 | use cyclonedx_bom::models::attached_text::AttachedText; 11 | use cyclonedx_bom::models::bom::{Bom, UrnUuid}; 12 | use cyclonedx_bom::models::code::{Diff, Patch, PatchClassification, Patches}; 13 | use cyclonedx_bom::models::component::Pedigree; 14 | use cyclonedx_bom::models::component::{Classification, Component, Components, Scope}; 15 | use cyclonedx_bom::models::external_reference::{ 16 | self, ExternalReference, ExternalReferenceType, ExternalReferences, 17 | }; 18 | use cyclonedx_bom::models::hash::{Hash, HashAlgorithm, HashValue, Hashes}; 19 | use cyclonedx_bom::models::license::{License, LicenseChoice, Licenses}; 20 | use cyclonedx_bom::models::metadata::Metadata; 21 | use cyclonedx_bom::models::tool::Tools; 22 | use sha2::{Digest, Sha256}; 23 | 24 | use crate::derivation::{self, Derivation, Meta, Src}; 25 | use crate::hash::{self, SriHash}; 26 | 27 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 28 | 29 | pub struct CycloneDXBom(Bom); 30 | 31 | impl CycloneDXBom { 32 | /// Serialize to JSON as bytes. 33 | pub fn serialize(self) -> Result> { 34 | let mut output = Vec::::new(); 35 | self.0.output_as_json_v1_5(&mut output)?; 36 | Ok(output) 37 | } 38 | 39 | /// Read a `CycloneDXBom` from a path. 40 | pub fn from_file(path: impl AsRef) -> Result { 41 | let file = fs::File::open(path)?; 42 | Ok(Self(Bom::parse_from_json(file)?)) 43 | } 44 | 45 | pub fn build(target: Derivation, components: CycloneDXComponents, output: &Path) -> Self { 46 | Self(Bom { 47 | components: Some(components.into()), 48 | metadata: Some(metadata_from_derivation(target)), 49 | // Derive a reproducible serial number from the output path. This works because the Nix 50 | // outPath of the derivation is input addressed and thus reproducible. 51 | serial_number: Some(derive_serial_number(output.as_os_str().as_encoded_bytes())), 52 | ..Bom::default() 53 | }) 54 | } 55 | 56 | fn components(self) -> Option { 57 | self.0.components 58 | } 59 | } 60 | 61 | /// Derive a serial number from some arbitrary data. 62 | /// 63 | /// This data is hashed with SHA256 and the first 16 bytes are used to create a UUID to serve as a 64 | /// serial number. 65 | fn derive_serial_number(data: &[u8]) -> UrnUuid { 66 | let hash = Sha256::digest(data); 67 | let array: [u8; 32] = hash.into(); 68 | #[allow(clippy::expect_used)] 69 | let bytes = array[..16] 70 | .try_into() 71 | .expect("Failed to extract 16 bytes from SHA256 hash"); 72 | let uuid = uuid::Builder::from_bytes(bytes).into_uuid(); 73 | UrnUuid::from(uuid) 74 | } 75 | 76 | pub struct CycloneDXComponents(Components); 77 | 78 | impl CycloneDXComponents { 79 | pub fn from_derivations(derivations: impl IntoIterator) -> Self { 80 | Self(Components( 81 | derivations 82 | .into_iter() 83 | .map(CycloneDXComponent::from_derivation) 84 | .map(CycloneDXComponent::into) 85 | .collect(), 86 | )) 87 | } 88 | 89 | /// Extend the `Components` with components read from multiple BOMs inside a directory. 90 | pub fn extend_from_directory(&mut self, path: impl AsRef) -> Result<()> { 91 | let mut m = BTreeMap::new(); 92 | 93 | // Insert the components from the original SBOM 94 | for component in self.0 .0.clone() { 95 | let key = component 96 | .bom_ref 97 | .clone() 98 | .unwrap_or_else(|| component.name.to_string()); 99 | m.entry(key).or_insert(component); 100 | } 101 | 102 | // Add the components from the vendored SBOMs 103 | for entry in fs::read_dir(&path) 104 | .with_context(|| format!("Failed to read {:?}", path.as_ref()))? 105 | .flatten() 106 | { 107 | let bom = CycloneDXBom::from_file(entry.path())?; 108 | if let Some(components) = bom.components() { 109 | for component in components.0 { 110 | let key = component 111 | .bom_ref 112 | .clone() 113 | .unwrap_or_else(|| component.name.to_string()); 114 | m.entry(key).or_insert(component); 115 | } 116 | } 117 | } 118 | 119 | self.0 .0 = m.into_values().collect(); 120 | Ok(()) 121 | } 122 | } 123 | 124 | impl From for Components { 125 | fn from(value: CycloneDXComponents) -> Self { 126 | value.0 127 | } 128 | } 129 | 130 | struct CycloneDXComponent(Component); 131 | 132 | impl CycloneDXComponent { 133 | fn from_derivation(derivation: Derivation) -> Self { 134 | let name = match derivation.pname { 135 | Some(pname) => pname, 136 | None => derivation.name.unwrap_or_default(), 137 | }; 138 | let version = derivation.version.unwrap_or_default(); 139 | let mut component = Component::new( 140 | // Classification::Application is used as per specification when the type is not known 141 | // as is the case for dependencies from Nix 142 | Classification::Application, 143 | &name, 144 | &version, 145 | Some( 146 | derivation 147 | .path 148 | .strip_prefix("/nix/store/") 149 | .unwrap_or(&derivation.path) 150 | .to_string(), 151 | ), 152 | ); 153 | component.scope = Some(Scope::Required); 154 | component.purl = Purl::new("nix", &name, &version).ok(); 155 | component.hashes = derivation.output_hash.and_then(|s| convert_hash(&s)); 156 | 157 | let mut external_references = Vec::new(); 158 | 159 | if let Some(src) = derivation.src { 160 | if !src.urls.is_empty() { 161 | external_references.extend(convert_src(&src)); 162 | } 163 | } 164 | if let Some(meta) = derivation.meta { 165 | component.licenses = convert_licenses(&meta); 166 | component.description = meta.description.map(|s| NormalizedString::new(&s)); 167 | if let Some(homepage) = meta.homepage { 168 | external_references.push(convert_homepage(&homepage)); 169 | } 170 | } 171 | 172 | if !external_references.is_empty() { 173 | component.external_references = Some(ExternalReferences(external_references)); 174 | } 175 | 176 | if !derivation.patches.is_empty() { 177 | component.pedigree = Some(Pedigree { 178 | ancestors: None, 179 | descendants: None, 180 | variants: None, 181 | commits: None, 182 | patches: Some(convert_patches(&derivation.patches)), 183 | notes: None, 184 | }); 185 | } 186 | 187 | Self(component) 188 | } 189 | } 190 | 191 | impl From for Component { 192 | fn from(value: CycloneDXComponent) -> Self { 193 | value.0 194 | } 195 | } 196 | 197 | fn string_to_url(s: &str) -> external_reference::Uri { 198 | external_reference::Uri::Url(Uri::new(s)) 199 | } 200 | 201 | fn convert_licenses(meta: &Meta) -> Option { 202 | Some(Licenses(match &meta.license { 203 | Some(license) => license 204 | .clone() 205 | .into_vec() 206 | .into_iter() 207 | .map(convert_license) 208 | .collect(), 209 | _ => return None, 210 | })) 211 | } 212 | 213 | fn convert_src(src: &Src) -> Vec { 214 | assert!( 215 | !src.urls.is_empty(), 216 | "src.urls must contain at least one value to generate ExternalReference", 217 | ); 218 | assert!( 219 | !src.urls.iter().any(String::is_empty), 220 | "All urls in src.urls must not be empty strings to generate ExternalReference", 221 | ); 222 | src.urls 223 | .iter() 224 | .map(|u| ExternalReference { 225 | external_reference_type: ExternalReferenceType::Vcs, 226 | url: string_to_url(u), 227 | comment: None, 228 | hashes: src.hash.clone().and_then(|s| convert_hash(&s)), 229 | }) 230 | .collect() 231 | } 232 | 233 | impl From for HashAlgorithm { 234 | fn from(value: hash::Algorithm) -> Self { 235 | match value { 236 | hash::Algorithm::Md5 => HashAlgorithm::MD5, 237 | hash::Algorithm::Sha1 => HashAlgorithm::SHA1, 238 | hash::Algorithm::Sha256 => HashAlgorithm::SHA_256, 239 | hash::Algorithm::Sha512 => HashAlgorithm::SHA_512, 240 | } 241 | } 242 | } 243 | 244 | fn convert_hash(s: &str) -> Option { 245 | // If it's not an SRI hash, we'll return None 246 | let sri_hash = SriHash::from_str(s).ok()?; 247 | let hash = Hash { 248 | content: HashValue(sri_hash.hex_digest()), 249 | alg: sri_hash.algorithm.into(), 250 | }; 251 | Some(Hashes(vec![hash])) 252 | } 253 | 254 | fn convert_license(license: derivation::License) -> LicenseChoice { 255 | match license.spdx_id { 256 | Some(spdx_id) => LicenseChoice::License(License::license_id(&spdx_id)), 257 | None => LicenseChoice::License(License::named_license(&license.full_name)), 258 | } 259 | } 260 | 261 | fn convert_homepage(homepage: &str) -> ExternalReference { 262 | ExternalReference { 263 | external_reference_type: ExternalReferenceType::Website, 264 | url: string_to_url(homepage), 265 | comment: None, 266 | hashes: None, 267 | } 268 | } 269 | 270 | fn metadata_from_derivation(derivation: Derivation) -> Metadata { 271 | Metadata { 272 | timestamp: None, 273 | tools: Some(metadata_tools()), 274 | authors: None, 275 | component: Some(CycloneDXComponent::from_derivation(derivation).into()), 276 | manufacture: None, 277 | supplier: None, 278 | licenses: None, 279 | properties: None, 280 | lifecycles: None, 281 | } 282 | } 283 | 284 | fn metadata_tools() -> Tools { 285 | let mut component = Component::new(Classification::Application, "bombon", VERSION, None); 286 | component.external_references = Some(ExternalReferences(vec![convert_homepage( 287 | "https://github.com/nikstur/bombon", 288 | )])); 289 | component.description = Some(NormalizedString::new( 290 | "Nix CycloneDX Software Bills of Materials (SBOMs)", 291 | )); 292 | component.licenses = Some(Licenses(vec![LicenseChoice::License(License::license_id( 293 | "MIT", 294 | ))])); 295 | 296 | Tools::Object { 297 | services: None, 298 | components: Some(Components(vec![component])), 299 | } 300 | } 301 | 302 | fn convert_patches(patches: &[String]) -> Patches { 303 | let cyclonedx_patches = patches 304 | .iter() 305 | .filter_map(|patch| fs::read_to_string(patch).ok()) 306 | .map(|diff| Patch { 307 | // As we know nothing about the patch at this level, the safest is to assume that it's 308 | // unofficial 309 | patch_type: PatchClassification::Unofficial, 310 | diff: Some(Diff { 311 | text: Some(AttachedText { 312 | content_type: Some(NormalizedString::new("text/plain")), 313 | encoding: None, 314 | content: diff, 315 | }), 316 | url: None, 317 | }), 318 | resolves: None, 319 | }) 320 | .collect::>(); 321 | Patches(cyclonedx_patches) 322 | } 323 | -------------------------------------------------------------------------------- /rust/transformer/src/derivation.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use serde::Deserialize; 3 | 4 | #[derive(Deserialize, Clone, Debug, Default)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct Derivation { 7 | pub path: String, 8 | pub name: Option, 9 | pub pname: Option, 10 | pub version: Option, 11 | pub meta: Option, 12 | pub output_name: Option, 13 | pub output_hash: Option, 14 | pub src: Option, 15 | pub vendored_sbom: Option, 16 | pub patches: Vec, 17 | } 18 | 19 | impl Derivation { 20 | /// Create a `Derivation` from a store path. 21 | /// 22 | /// This can be used if we don't have any information besides the path itself. 23 | pub fn from_store_path(store_path: &str) -> Self { 24 | // Because we only have the store path we have to derive the name from it 25 | let name = store_path.strip_prefix("/nix/store/").map(|s| { 26 | let mut split = s.split('-'); 27 | split.next(); 28 | split.join("-") 29 | }); 30 | 31 | Self { 32 | path: store_path.to_string(), 33 | name, 34 | ..Self::default() 35 | } 36 | } 37 | } 38 | 39 | #[derive(Deserialize, Clone, Debug)] 40 | pub struct Meta { 41 | pub license: Option, 42 | pub homepage: Option, 43 | pub description: Option, 44 | } 45 | 46 | #[derive(Deserialize, Clone, Debug)] 47 | #[serde(untagged)] 48 | pub enum LicenseField { 49 | LicenseList(LicenseList), 50 | License(License), 51 | // In very rare cases the license is just a String. 52 | // This mostly serves as a fallback so that serde doesn't panic. 53 | String(String), 54 | } 55 | 56 | impl LicenseField { 57 | pub fn into_vec(self) -> Vec { 58 | match self { 59 | Self::LicenseList(license_list) => license_list.0, 60 | Self::License(license) => vec![license], 61 | // Fallback to handle very unusual license fields in Nix. 62 | Self::String(_) => vec![], 63 | } 64 | } 65 | } 66 | 67 | #[derive(Deserialize, Clone, Debug)] 68 | pub struct LicenseList(Vec); 69 | 70 | #[derive(Deserialize, Clone, Debug)] 71 | pub struct License { 72 | #[serde(rename = "fullName")] 73 | pub full_name: String, 74 | #[serde(rename = "spdxId")] 75 | pub spdx_id: Option, 76 | } 77 | 78 | #[derive(Deserialize, Clone, Debug)] 79 | pub struct Src { 80 | pub urls: Vec, 81 | pub hash: Option, 82 | } 83 | -------------------------------------------------------------------------------- /rust/transformer/src/hash.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | use std::str::FromStr; 3 | 4 | use anyhow::{anyhow, bail, Error, Result}; 5 | use base64::prelude::{Engine as _, BASE64_STANDARD}; 6 | 7 | #[derive(Debug, Clone)] 8 | #[allow(clippy::module_name_repetitions)] 9 | pub struct SriHash { 10 | pub algorithm: Algorithm, 11 | pub digest: Vec, 12 | } 13 | 14 | #[derive(Debug, Clone)] 15 | pub enum Algorithm { 16 | Md5, 17 | Sha1, 18 | Sha256, 19 | Sha512, 20 | } 21 | 22 | impl FromStr for SriHash { 23 | type Err = Error; 24 | 25 | fn from_str(s: &str) -> Result { 26 | let mut parsed = s.trim().split('-'); 27 | 28 | let algorithm: Algorithm = parsed 29 | .next() 30 | .and_then(|s| FromStr::from_str(s).ok()) 31 | .ok_or(anyhow!("Failed to parse hash algorithm"))?; 32 | 33 | let digest = parsed 34 | .next() 35 | .and_then(|s| BASE64_STANDARD.decode(s).ok()) 36 | .ok_or(anyhow!("Failed to decode hash digest"))?; 37 | 38 | Ok(Self { algorithm, digest }) 39 | } 40 | } 41 | 42 | impl FromStr for Algorithm { 43 | type Err = Error; 44 | 45 | fn from_str(s: &str) -> Result { 46 | let matched = match s { 47 | "md5" => Self::Md5, 48 | "sha1" => Self::Sha1, 49 | "sha256" => Self::Sha256, 50 | "sha512" => Self::Sha512, 51 | _ => bail!("Failed to parse hash algorithm"), 52 | }; 53 | Ok(matched) 54 | } 55 | } 56 | 57 | impl SriHash { 58 | /// Return the digest as a lower hex encoded string. 59 | pub fn hex_digest(&self) -> String { 60 | let mut buffer = String::new(); 61 | for byte in &self.digest { 62 | let _ = write!(&mut buffer, "{byte:02x}"); 63 | } 64 | buffer 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /rust/transformer/src/main.rs: -------------------------------------------------------------------------------- 1 | mod buildtime_input; 2 | mod cli; 3 | mod cyclonedx; 4 | mod derivation; 5 | mod hash; 6 | mod runtime_input; 7 | mod transform; 8 | 9 | use anyhow::Result; 10 | use clap::Parser; 11 | 12 | use cli::Cli; 13 | 14 | fn main() -> Result<()> { 15 | Cli::parse().call() 16 | } 17 | -------------------------------------------------------------------------------- /rust/transformer/src/runtime_input.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::ToOwned; 2 | use std::collections::BTreeSet; 3 | use std::fs; 4 | use std::path::Path; 5 | 6 | use anyhow::{Context, Result}; 7 | 8 | pub struct RuntimeInput(pub BTreeSet); 9 | 10 | impl RuntimeInput { 11 | pub fn from_file(path: &Path) -> Result { 12 | let file_content = 13 | fs::read_to_string(path).with_context(|| format!("Failed to read {path:?}"))?; 14 | let set: BTreeSet = file_content.lines().map(ToOwned::to_owned).collect(); 15 | Ok(Self(set)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /rust/transformer/src/transform.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::Write; 3 | use std::path::Path; 4 | 5 | use anyhow::{Context, Result}; 6 | use itertools::Itertools; 7 | use regex::RegexSet; 8 | 9 | use crate::buildtime_input::BuildtimeInput; 10 | use crate::cyclonedx::{CycloneDXBom, CycloneDXComponents}; 11 | use crate::derivation::Derivation; 12 | use crate::runtime_input::RuntimeInput; 13 | 14 | pub fn transform( 15 | include_buildtime_dependencies: bool, 16 | exclude: &[String], 17 | target_path: &str, 18 | buildtime_input_path: &Path, 19 | runtime_input_path: &Path, 20 | output: &Path, 21 | ) -> Result<()> { 22 | let buildtime_input = BuildtimeInput::from_file(buildtime_input_path)?; 23 | let target_derivation = buildtime_input 24 | .0 25 | .get(target_path) 26 | .map(ToOwned::to_owned) 27 | .with_context(|| { 28 | format!("Buildtime input doesn't contain target derivation: {target_path}") 29 | })?; 30 | 31 | let runtime_input = RuntimeInput::from_file(runtime_input_path)?; 32 | 33 | // Augment the runtime input with information from the buildtime input. The buildtime input, 34 | // however, is not a strict superset of the runtime input. This has to do with how we query the 35 | // buildinputs from Nix and how dependencies can "hide" in String Contexts. 36 | let runtime_derivations = runtime_input.0.iter().map(|store_path| { 37 | buildtime_input 38 | .0 39 | .get(store_path) 40 | .map(ToOwned::to_owned) 41 | .unwrap_or(Derivation::from_store_path(store_path)) 42 | }); 43 | 44 | let buildtime_derivations = buildtime_input 45 | .0 46 | .clone() 47 | .into_values() 48 | .filter(|derivation| !runtime_input.0.contains(&derivation.path)) 49 | .unique_by(|d| d.name.clone().unwrap_or(d.path.clone())); 50 | 51 | let all_derivations: Box> = if include_buildtime_dependencies { 52 | Box::new(runtime_derivations.chain(buildtime_derivations)) 53 | } else { 54 | Box::new(runtime_derivations) 55 | }; 56 | 57 | let set = RegexSet::new(exclude).context("Failed to build regex set from exclude patterns")?; 58 | 59 | let all_derivations = all_derivations 60 | // Filter out all doc and man outputs. 61 | .filter(|derivation| { 62 | !matches!( 63 | derivation.output_name.clone().unwrap_or_default().as_ref(), 64 | "doc" | "man" 65 | ) 66 | }) 67 | // Filter out derivations that match one of the exclude patterns. 68 | .filter(|derivation| !set.is_match(&derivation.path)); 69 | 70 | let mut components = CycloneDXComponents::from_derivations(all_derivations); 71 | 72 | // Augment the components with those retrieved from the `sbom` passthru attribute of the 73 | // derivations. 74 | for derivation in buildtime_input.0.values() { 75 | if let Some(sbom_path) = &derivation.vendored_sbom { 76 | components.extend_from_directory(sbom_path)?; 77 | } 78 | } 79 | 80 | let bom = CycloneDXBom::build(target_derivation, components, output); 81 | let mut file = 82 | File::create(output).with_context(|| format!("Failed to create file {output:?}"))?; 83 | file.write_all(&bom.serialize()?)?; 84 | 85 | Ok(()) 86 | } 87 | --------------------------------------------------------------------------------