├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── LICENSE-ZLIB ├── README.md ├── deps_tests ├── README.md ├── cargo_0.70.1.deps.json ├── cargo_0.70.1.deps_no_dev.json ├── cargo_0.70.1.metadata.json ├── snapbox_0.4.11.deps.json ├── snapbox_0.4.11.deps_no_dev.json └── snapbox_0.4.11.metadata.json └── src ├── api_client.rs ├── cli.rs ├── common.rs ├── crates_cache.rs ├── main.rs ├── publishers.rs └── subcommands ├── crates.rs ├── json.rs ├── json_schema.rs ├── mod.rs ├── publishers.rs └── update.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust CI 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | rust: [stable] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - run: rustup default ${{ matrix.rust }} 16 | - name: build 17 | run: > 18 | cargo build --verbose 19 | - name: test 20 | run: > 21 | cargo test --tests 22 | rustfmt: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions-rs/toolchain@v1 27 | with: 28 | toolchain: stable 29 | override: true 30 | components: rustfmt 31 | - name: Run rustfmt check 32 | uses: actions-rs/cargo@v1 33 | with: 34 | command: fmt 35 | args: -- --check 36 | doc: 37 | runs-on: ubuntu-latest 38 | strategy: 39 | matrix: 40 | rust: [stable] 41 | steps: 42 | - uses: actions/checkout@v2 43 | - run: rustup default ${{ matrix.rust }} 44 | - name: doc 45 | run: > 46 | cargo doc --no-deps --document-private-items --all-features 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.3.3 (2023-05-08) 2 | 3 | - Add `--no-dev` flag to omit dev dependencies (contribution by @smoelius) 4 | 5 | ## v0.3.2 (2022-11-04) 6 | 7 | - Upgrade to `bpaf` 0.7 8 | 9 | ## v0.3.1 (2021-03-18) 10 | 11 | - Fix `--features` flag not being honored if `--target` is also passed 12 | 13 | ## v0.3.0 (2021-03-18) 14 | 15 | - Renamed `--cache_max_age` to `--cache-max-age` for consistency with Cargo flags 16 | - Accept flags such as `--target` directly, without relying on the escape hatch of passing cargo metadata arguments after `--` 17 | - No longer default to `--all-features`, handle features via the same flags as Cargo itself 18 | - The json schema is now printed separately, use `cargo supply-chain json --print-schema` to get it 19 | - Dropped the `help` subcommand. Use `--help` instead, e.g. `cargo supply-chain crates --help` 20 | 21 | Internal improvements: 22 | 23 | - Migrate to bpaf CLI parser, chosen for its balance of expressiveness vs complexity and supply chain sprawl 24 | - Add tests for the CLI interface 25 | - Do not regenerate the JSON schema on every build; saves a bit of build time and a bit of dependencies in production builds 26 | 27 | ## v0.2.0 (2021-05-21) 28 | 29 | - Added `json` subcommand providing structured output and more details 30 | - Added `-d`, `--diffable` flag for diff-friendly output mode to all subcommands 31 | - Reduced the required download size for `update` subcommand from ~350Mb to ~60Mb 32 | - Added a detailed progress bar to all subcommands using `indicatif` 33 | - Fixed interrupted `update` subcommand considering its cache to be fresh. 34 | Other subcommands were not affected and would simply fetch live data. 35 | - If a call to `cargo metadata` fails, show an error instead of panicking 36 | - The list of crates in the output of `publishers` subcommand is now sorted 37 | 38 | ## v0.1.2 (2021-02-24) 39 | 40 | - Fix help text sometimes being misaligned 41 | - Change download progress messages to start counting from 1 rather than from 0 42 | - Only print warnings about crates.io that are immediately relevant to listing 43 | dependencies and publishers 44 | 45 | ## v0.1.1 (2021-02-18) 46 | 47 | - Drop extreaneous files from the tarball uploaded to crates.io 48 | 49 | ## v0.1.0 (2021-02-18) 50 | 51 | - Drop `authors` subcommand 52 | - Add `help` subcommand providing detailed help for each subcommand 53 | - Bring help text more in line with Cargo help text 54 | - Warn about a large amount of data to be downloaded in `update` subcommand 55 | - Buffer reads and writes to cache files for a 6x speedup when using cache 56 | 57 | ## v0.0.4 (2021-01-01) 58 | 59 | - Report failure instead of panicking on network failure in `update` subcommand 60 | - Correctly handle errors returned by the remote server 61 | 62 | ## v0.0.3 (2020-12-28) 63 | 64 | - In case of network failure, retry with exponential backoff up to 3 times 65 | - Use local certificate store instead of bundling the trusted CA certificates 66 | - Refactor argument parsing to use `pico-args` instead of hand-rolled parser 67 | 68 | ## v0.0.2 (2020-10-14) 69 | 70 | - `crates` - Shows the people or groups with publisher rights for each crate. 71 | - `publishers` - Is the reverse of `crates`, grouping by publisher instead. 72 | - `update` - Caches the data dumps from `crates.io` to avoid crawling the web 73 | service when lookup up publisher and author information. 74 | 75 | ## v0.0.1 (2020-10-02) 76 | 77 | Initial release, supports one command: 78 | - `authors` - Crawl through Cargo.toml of all crates and list their authors. 79 | Authors might be listed multiple times. For each author, differentiate if 80 | they are known by being mentioned in a crate from the local workspace or not. 81 | Support for crawling `crates.io` sourced packages is planned. 82 | - `publishers` - Doesn't do anything right now. 83 | -------------------------------------------------------------------------------- /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 = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "anyhow" 13 | version = "1.0.58" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" 16 | 17 | [[package]] 18 | name = "base64" 19 | version = "0.13.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 22 | 23 | [[package]] 24 | name = "bitflags" 25 | version = "1.3.2" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 28 | 29 | [[package]] 30 | name = "bitflags" 31 | version = "2.3.3" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" 34 | 35 | [[package]] 36 | name = "bpaf" 37 | version = "0.9.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "72974597bfc83173d714c0fc785a8ab64ca0f0896cb72b05f2f4c5e682543871" 40 | dependencies = [ 41 | "bpaf_derive", 42 | "owo-colors", 43 | "supports-color", 44 | ] 45 | 46 | [[package]] 47 | name = "bpaf_derive" 48 | version = "0.5.1" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "f6b7be5dcfd7bb931b9932e689c69a9b9f50a46cf0b588c90ed73ec28e8e0bf4" 51 | dependencies = [ 52 | "proc-macro2", 53 | "quote", 54 | "syn 2.0.23", 55 | ] 56 | 57 | [[package]] 58 | name = "bstr" 59 | version = "0.2.17" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" 62 | dependencies = [ 63 | "lazy_static", 64 | "memchr", 65 | "regex-automata", 66 | "serde", 67 | ] 68 | 69 | [[package]] 70 | name = "bumpalo" 71 | version = "3.10.0" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" 74 | 75 | [[package]] 76 | name = "camino" 77 | version = "1.0.9" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "869119e97797867fd90f5e22af7d0bd274bd4635ebb9eb68c04f3f513ae6c412" 80 | dependencies = [ 81 | "serde", 82 | ] 83 | 84 | [[package]] 85 | name = "cargo-platform" 86 | version = "0.1.2" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" 89 | dependencies = [ 90 | "serde", 91 | ] 92 | 93 | [[package]] 94 | name = "cargo-supply-chain" 95 | version = "0.3.3" 96 | dependencies = [ 97 | "anyhow", 98 | "bpaf", 99 | "cargo_metadata", 100 | "csv", 101 | "flate2", 102 | "humantime", 103 | "humantime-serde", 104 | "indicatif", 105 | "schemars", 106 | "serde", 107 | "serde_json", 108 | "tar", 109 | "ureq", 110 | "xdg", 111 | ] 112 | 113 | [[package]] 114 | name = "cargo_metadata" 115 | version = "0.15.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "3abb7553d5b9b8421c6de7cb02606ff15e0c6eea7d8eadd75ef013fd636bec36" 118 | dependencies = [ 119 | "camino", 120 | "cargo-platform", 121 | "semver", 122 | "serde", 123 | "serde_json", 124 | ] 125 | 126 | [[package]] 127 | name = "cc" 128 | version = "1.0.73" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 131 | 132 | [[package]] 133 | name = "cfg-if" 134 | version = "1.0.0" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 137 | 138 | [[package]] 139 | name = "chunked_transfer" 140 | version = "1.4.0" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" 143 | 144 | [[package]] 145 | name = "console" 146 | version = "0.15.1" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847" 149 | dependencies = [ 150 | "encode_unicode", 151 | "libc", 152 | "once_cell", 153 | "terminal_size", 154 | "unicode-width", 155 | "winapi", 156 | ] 157 | 158 | [[package]] 159 | name = "core-foundation" 160 | version = "0.9.3" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 163 | dependencies = [ 164 | "core-foundation-sys", 165 | "libc", 166 | ] 167 | 168 | [[package]] 169 | name = "core-foundation-sys" 170 | version = "0.8.3" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 173 | 174 | [[package]] 175 | name = "crc32fast" 176 | version = "1.3.2" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 179 | dependencies = [ 180 | "cfg-if", 181 | ] 182 | 183 | [[package]] 184 | name = "csv" 185 | version = "1.1.6" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" 188 | dependencies = [ 189 | "bstr", 190 | "csv-core", 191 | "itoa 0.4.8", 192 | "ryu", 193 | "serde", 194 | ] 195 | 196 | [[package]] 197 | name = "csv-core" 198 | version = "0.1.10" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" 201 | dependencies = [ 202 | "memchr", 203 | ] 204 | 205 | [[package]] 206 | name = "dyn-clone" 207 | version = "1.0.8" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "9d07a982d1fb29db01e5a59b1918e03da4df7297eaeee7686ac45542fd4e59c8" 210 | 211 | [[package]] 212 | name = "encode_unicode" 213 | version = "0.3.6" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 216 | 217 | [[package]] 218 | name = "errno" 219 | version = "0.3.1" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 222 | dependencies = [ 223 | "errno-dragonfly", 224 | "libc", 225 | "windows-sys 0.48.0", 226 | ] 227 | 228 | [[package]] 229 | name = "errno-dragonfly" 230 | version = "0.1.2" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 233 | dependencies = [ 234 | "cc", 235 | "libc", 236 | ] 237 | 238 | [[package]] 239 | name = "filetime" 240 | version = "0.2.17" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" 243 | dependencies = [ 244 | "cfg-if", 245 | "libc", 246 | "redox_syscall", 247 | "windows-sys 0.36.1", 248 | ] 249 | 250 | [[package]] 251 | name = "flate2" 252 | version = "1.0.24" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" 255 | dependencies = [ 256 | "crc32fast", 257 | "miniz_oxide", 258 | ] 259 | 260 | [[package]] 261 | name = "form_urlencoded" 262 | version = "1.0.1" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 265 | dependencies = [ 266 | "matches", 267 | "percent-encoding", 268 | ] 269 | 270 | [[package]] 271 | name = "hermit-abi" 272 | version = "0.3.2" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" 275 | 276 | [[package]] 277 | name = "humantime" 278 | version = "2.1.0" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 281 | 282 | [[package]] 283 | name = "humantime-serde" 284 | version = "1.1.1" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" 287 | dependencies = [ 288 | "humantime", 289 | "serde", 290 | ] 291 | 292 | [[package]] 293 | name = "idna" 294 | version = "0.2.3" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 297 | dependencies = [ 298 | "matches", 299 | "unicode-bidi", 300 | "unicode-normalization", 301 | ] 302 | 303 | [[package]] 304 | name = "indicatif" 305 | version = "0.17.0" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "fcc42b206e70d86ec03285b123e65a5458c92027d1fb2ae3555878b8113b3ddf" 308 | dependencies = [ 309 | "console", 310 | "number_prefix", 311 | "unicode-width", 312 | ] 313 | 314 | [[package]] 315 | name = "is-terminal" 316 | version = "0.4.8" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb" 319 | dependencies = [ 320 | "hermit-abi", 321 | "rustix", 322 | "windows-sys 0.48.0", 323 | ] 324 | 325 | [[package]] 326 | name = "is_ci" 327 | version = "1.1.1" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" 330 | 331 | [[package]] 332 | name = "itoa" 333 | version = "0.4.8" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" 336 | 337 | [[package]] 338 | name = "itoa" 339 | version = "1.0.2" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" 342 | 343 | [[package]] 344 | name = "js-sys" 345 | version = "0.3.59" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" 348 | dependencies = [ 349 | "wasm-bindgen", 350 | ] 351 | 352 | [[package]] 353 | name = "lazy_static" 354 | version = "1.4.0" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 357 | 358 | [[package]] 359 | name = "libc" 360 | version = "0.2.147" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 363 | 364 | [[package]] 365 | name = "linux-raw-sys" 366 | version = "0.4.3" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" 369 | 370 | [[package]] 371 | name = "log" 372 | version = "0.4.17" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 375 | dependencies = [ 376 | "cfg-if", 377 | ] 378 | 379 | [[package]] 380 | name = "matches" 381 | version = "0.1.9" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" 384 | 385 | [[package]] 386 | name = "memchr" 387 | version = "2.5.0" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 390 | 391 | [[package]] 392 | name = "miniz_oxide" 393 | version = "0.5.3" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" 396 | dependencies = [ 397 | "adler", 398 | ] 399 | 400 | [[package]] 401 | name = "number_prefix" 402 | version = "0.4.0" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 405 | 406 | [[package]] 407 | name = "once_cell" 408 | version = "1.13.0" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" 411 | 412 | [[package]] 413 | name = "openssl-probe" 414 | version = "0.1.5" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 417 | 418 | [[package]] 419 | name = "owo-colors" 420 | version = "3.5.0" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" 423 | 424 | [[package]] 425 | name = "percent-encoding" 426 | version = "2.1.0" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 429 | 430 | [[package]] 431 | name = "proc-macro2" 432 | version = "1.0.63" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" 435 | dependencies = [ 436 | "unicode-ident", 437 | ] 438 | 439 | [[package]] 440 | name = "quote" 441 | version = "1.0.29" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" 444 | dependencies = [ 445 | "proc-macro2", 446 | ] 447 | 448 | [[package]] 449 | name = "redox_syscall" 450 | version = "0.2.16" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 453 | dependencies = [ 454 | "bitflags 1.3.2", 455 | ] 456 | 457 | [[package]] 458 | name = "regex-automata" 459 | version = "0.1.10" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 462 | 463 | [[package]] 464 | name = "ring" 465 | version = "0.16.20" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" 468 | dependencies = [ 469 | "cc", 470 | "libc", 471 | "once_cell", 472 | "spin", 473 | "untrusted", 474 | "web-sys", 475 | "winapi", 476 | ] 477 | 478 | [[package]] 479 | name = "rustix" 480 | version = "0.38.3" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4" 483 | dependencies = [ 484 | "bitflags 2.3.3", 485 | "errno", 486 | "libc", 487 | "linux-raw-sys", 488 | "windows-sys 0.48.0", 489 | ] 490 | 491 | [[package]] 492 | name = "rustls" 493 | version = "0.20.6" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" 496 | dependencies = [ 497 | "log", 498 | "ring", 499 | "sct", 500 | "webpki", 501 | ] 502 | 503 | [[package]] 504 | name = "rustls-native-certs" 505 | version = "0.6.2" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" 508 | dependencies = [ 509 | "openssl-probe", 510 | "rustls-pemfile", 511 | "schannel", 512 | "security-framework", 513 | ] 514 | 515 | [[package]] 516 | name = "rustls-pemfile" 517 | version = "1.0.0" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9" 520 | dependencies = [ 521 | "base64", 522 | ] 523 | 524 | [[package]] 525 | name = "ryu" 526 | version = "1.0.10" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" 529 | 530 | [[package]] 531 | name = "schannel" 532 | version = "0.1.20" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" 535 | dependencies = [ 536 | "lazy_static", 537 | "windows-sys 0.36.1", 538 | ] 539 | 540 | [[package]] 541 | name = "schemars" 542 | version = "0.8.10" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "1847b767a3d62d95cbf3d8a9f0e421cf57a0d8aa4f411d4b16525afb0284d4ed" 545 | dependencies = [ 546 | "dyn-clone", 547 | "schemars_derive", 548 | "serde", 549 | "serde_json", 550 | ] 551 | 552 | [[package]] 553 | name = "schemars_derive" 554 | version = "0.8.10" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "af4d7e1b012cb3d9129567661a63755ea4b8a7386d339dc945ae187e403c6743" 557 | dependencies = [ 558 | "proc-macro2", 559 | "quote", 560 | "serde_derive_internals", 561 | "syn 1.0.98", 562 | ] 563 | 564 | [[package]] 565 | name = "sct" 566 | version = "0.7.0" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" 569 | dependencies = [ 570 | "ring", 571 | "untrusted", 572 | ] 573 | 574 | [[package]] 575 | name = "security-framework" 576 | version = "2.6.1" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" 579 | dependencies = [ 580 | "bitflags 1.3.2", 581 | "core-foundation", 582 | "core-foundation-sys", 583 | "libc", 584 | "security-framework-sys", 585 | ] 586 | 587 | [[package]] 588 | name = "security-framework-sys" 589 | version = "2.6.1" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" 592 | dependencies = [ 593 | "core-foundation-sys", 594 | "libc", 595 | ] 596 | 597 | [[package]] 598 | name = "semver" 599 | version = "1.0.12" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" 602 | dependencies = [ 603 | "serde", 604 | ] 605 | 606 | [[package]] 607 | name = "serde" 608 | version = "1.0.140" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "fc855a42c7967b7c369eb5860f7164ef1f6f81c20c7cc1141f2a604e18723b03" 611 | dependencies = [ 612 | "serde_derive", 613 | ] 614 | 615 | [[package]] 616 | name = "serde_derive" 617 | version = "1.0.140" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da" 620 | dependencies = [ 621 | "proc-macro2", 622 | "quote", 623 | "syn 1.0.98", 624 | ] 625 | 626 | [[package]] 627 | name = "serde_derive_internals" 628 | version = "0.26.0" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" 631 | dependencies = [ 632 | "proc-macro2", 633 | "quote", 634 | "syn 1.0.98", 635 | ] 636 | 637 | [[package]] 638 | name = "serde_json" 639 | version = "1.0.82" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" 642 | dependencies = [ 643 | "itoa 1.0.2", 644 | "ryu", 645 | "serde", 646 | ] 647 | 648 | [[package]] 649 | name = "spin" 650 | version = "0.5.2" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 653 | 654 | [[package]] 655 | name = "supports-color" 656 | version = "2.0.0" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "4950e7174bffabe99455511c39707310e7e9b440364a2fcb1cc21521be57b354" 659 | dependencies = [ 660 | "is-terminal", 661 | "is_ci", 662 | ] 663 | 664 | [[package]] 665 | name = "syn" 666 | version = "1.0.98" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" 669 | dependencies = [ 670 | "proc-macro2", 671 | "quote", 672 | "unicode-ident", 673 | ] 674 | 675 | [[package]] 676 | name = "syn" 677 | version = "2.0.23" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" 680 | dependencies = [ 681 | "proc-macro2", 682 | "quote", 683 | "unicode-ident", 684 | ] 685 | 686 | [[package]] 687 | name = "tar" 688 | version = "0.4.38" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" 691 | dependencies = [ 692 | "filetime", 693 | "libc", 694 | "xattr", 695 | ] 696 | 697 | [[package]] 698 | name = "terminal_size" 699 | version = "0.1.17" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" 702 | dependencies = [ 703 | "libc", 704 | "winapi", 705 | ] 706 | 707 | [[package]] 708 | name = "tinyvec" 709 | version = "1.6.0" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 712 | dependencies = [ 713 | "tinyvec_macros", 714 | ] 715 | 716 | [[package]] 717 | name = "tinyvec_macros" 718 | version = "0.1.0" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 721 | 722 | [[package]] 723 | name = "unicode-bidi" 724 | version = "0.3.8" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 727 | 728 | [[package]] 729 | name = "unicode-ident" 730 | version = "1.0.2" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" 733 | 734 | [[package]] 735 | name = "unicode-normalization" 736 | version = "0.1.21" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" 739 | dependencies = [ 740 | "tinyvec", 741 | ] 742 | 743 | [[package]] 744 | name = "unicode-width" 745 | version = "0.1.9" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 748 | 749 | [[package]] 750 | name = "untrusted" 751 | version = "0.7.1" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 754 | 755 | [[package]] 756 | name = "ureq" 757 | version = "2.5.0" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "b97acb4c28a254fd7a4aeec976c46a7fa404eac4d7c134b30c75144846d7cb8f" 760 | dependencies = [ 761 | "base64", 762 | "chunked_transfer", 763 | "log", 764 | "once_cell", 765 | "rustls", 766 | "rustls-native-certs", 767 | "serde", 768 | "serde_json", 769 | "url", 770 | "webpki", 771 | "webpki-roots", 772 | ] 773 | 774 | [[package]] 775 | name = "url" 776 | version = "2.2.2" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 779 | dependencies = [ 780 | "form_urlencoded", 781 | "idna", 782 | "matches", 783 | "percent-encoding", 784 | ] 785 | 786 | [[package]] 787 | name = "wasm-bindgen" 788 | version = "0.2.82" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" 791 | dependencies = [ 792 | "cfg-if", 793 | "wasm-bindgen-macro", 794 | ] 795 | 796 | [[package]] 797 | name = "wasm-bindgen-backend" 798 | version = "0.2.82" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" 801 | dependencies = [ 802 | "bumpalo", 803 | "log", 804 | "once_cell", 805 | "proc-macro2", 806 | "quote", 807 | "syn 1.0.98", 808 | "wasm-bindgen-shared", 809 | ] 810 | 811 | [[package]] 812 | name = "wasm-bindgen-macro" 813 | version = "0.2.82" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" 816 | dependencies = [ 817 | "quote", 818 | "wasm-bindgen-macro-support", 819 | ] 820 | 821 | [[package]] 822 | name = "wasm-bindgen-macro-support" 823 | version = "0.2.82" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" 826 | dependencies = [ 827 | "proc-macro2", 828 | "quote", 829 | "syn 1.0.98", 830 | "wasm-bindgen-backend", 831 | "wasm-bindgen-shared", 832 | ] 833 | 834 | [[package]] 835 | name = "wasm-bindgen-shared" 836 | version = "0.2.82" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" 839 | 840 | [[package]] 841 | name = "web-sys" 842 | version = "0.3.59" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" 845 | dependencies = [ 846 | "js-sys", 847 | "wasm-bindgen", 848 | ] 849 | 850 | [[package]] 851 | name = "webpki" 852 | version = "0.22.0" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" 855 | dependencies = [ 856 | "ring", 857 | "untrusted", 858 | ] 859 | 860 | [[package]] 861 | name = "webpki-roots" 862 | version = "0.22.4" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" 865 | dependencies = [ 866 | "webpki", 867 | ] 868 | 869 | [[package]] 870 | name = "winapi" 871 | version = "0.3.9" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 874 | dependencies = [ 875 | "winapi-i686-pc-windows-gnu", 876 | "winapi-x86_64-pc-windows-gnu", 877 | ] 878 | 879 | [[package]] 880 | name = "winapi-i686-pc-windows-gnu" 881 | version = "0.4.0" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 884 | 885 | [[package]] 886 | name = "winapi-x86_64-pc-windows-gnu" 887 | version = "0.4.0" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 890 | 891 | [[package]] 892 | name = "windows-sys" 893 | version = "0.36.1" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 896 | dependencies = [ 897 | "windows_aarch64_msvc 0.36.1", 898 | "windows_i686_gnu 0.36.1", 899 | "windows_i686_msvc 0.36.1", 900 | "windows_x86_64_gnu 0.36.1", 901 | "windows_x86_64_msvc 0.36.1", 902 | ] 903 | 904 | [[package]] 905 | name = "windows-sys" 906 | version = "0.48.0" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 909 | dependencies = [ 910 | "windows-targets", 911 | ] 912 | 913 | [[package]] 914 | name = "windows-targets" 915 | version = "0.48.1" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" 918 | dependencies = [ 919 | "windows_aarch64_gnullvm", 920 | "windows_aarch64_msvc 0.48.0", 921 | "windows_i686_gnu 0.48.0", 922 | "windows_i686_msvc 0.48.0", 923 | "windows_x86_64_gnu 0.48.0", 924 | "windows_x86_64_gnullvm", 925 | "windows_x86_64_msvc 0.48.0", 926 | ] 927 | 928 | [[package]] 929 | name = "windows_aarch64_gnullvm" 930 | version = "0.48.0" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 933 | 934 | [[package]] 935 | name = "windows_aarch64_msvc" 936 | version = "0.36.1" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 939 | 940 | [[package]] 941 | name = "windows_aarch64_msvc" 942 | version = "0.48.0" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 945 | 946 | [[package]] 947 | name = "windows_i686_gnu" 948 | version = "0.36.1" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 951 | 952 | [[package]] 953 | name = "windows_i686_gnu" 954 | version = "0.48.0" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 957 | 958 | [[package]] 959 | name = "windows_i686_msvc" 960 | version = "0.36.1" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 963 | 964 | [[package]] 965 | name = "windows_i686_msvc" 966 | version = "0.48.0" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 969 | 970 | [[package]] 971 | name = "windows_x86_64_gnu" 972 | version = "0.36.1" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 975 | 976 | [[package]] 977 | name = "windows_x86_64_gnu" 978 | version = "0.48.0" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 981 | 982 | [[package]] 983 | name = "windows_x86_64_gnullvm" 984 | version = "0.48.0" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 987 | 988 | [[package]] 989 | name = "windows_x86_64_msvc" 990 | version = "0.36.1" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 993 | 994 | [[package]] 995 | name = "windows_x86_64_msvc" 996 | version = "0.48.0" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 999 | 1000 | [[package]] 1001 | name = "xattr" 1002 | version = "0.2.3" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" 1005 | dependencies = [ 1006 | "libc", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "xdg" 1011 | version = "2.5.2" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" 1014 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-supply-chain" 3 | version = "0.3.3" 4 | description = "Gather author, contributor, publisher data on crates in your dependency graph" 5 | repository = "https://github.com/rust-secure-code/cargo-supply-chain" 6 | authors = ["Andreas Molzer ", "Sergey \"Shnatsel\" Davidoff "] 7 | edition = "2018" 8 | license = "Apache-2.0 OR MIT OR Zlib" 9 | categories = ["development-tools::cargo-plugins", "command-line-utilities"] 10 | exclude = ["deps_tests/"] 11 | 12 | [dependencies] 13 | cargo_metadata = "0.15.0" 14 | csv = "1.1" 15 | flate2 = "1" 16 | humantime = "2" 17 | humantime-serde = "1" 18 | ureq = { version = "2.0.1", default-features=false, features = ["tls", "native-certs", "json"] } 19 | serde = { version = "1.0", features = ["derive"] } 20 | serde_json = "1.0" 21 | tar = "0.4.30" 22 | indicatif = "0.17.0" 23 | bpaf = { version = "0.9.1", features = ["derive", "dull-color"] } 24 | anyhow = "1.0.28" 25 | xdg = "2.5" 26 | 27 | [dev-dependencies] 28 | schemars = "0.8.3" 29 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andreas Molzer aka. HeroicKatora 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 | -------------------------------------------------------------------------------- /LICENSE-ZLIB: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Andreas Molzer aka. HeroicKatora 2 | 3 | This software is provided 'as-is', without any express or implied warranty. In 4 | no event will the authors be held liable for any damages arising from the use 5 | of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, including 8 | commercial applications, and to alter it and redistribute it freely, subject to 9 | the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not claim 12 | that you wrote the original software. If you use this software in a product, an 13 | acknowledgment in the product documentation would be appreciated but is not 14 | required. 15 | 16 | 2. Altered source versions must be plainly marked as such, and must not be 17 | misrepresented as being the original software. 18 | 19 | 3. This notice may not be removed or altered from any source distribution. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cargo-supply-chain 2 | 3 | Gather author, contributor and publisher data on crates in your dependency graph. 4 | 5 | Use cases include: 6 | 7 | - Find people and groups worth supporting. 8 | - Identify risks in your dependency graph. 9 | - An analysis of all the contributors you implicitly trust by building their software. This might have both a sobering and humbling effect. 10 | 11 | Sample output when run on itself: [`publishers`](https://gist.github.com/Shnatsel/3b7f7d331d944bb75b2f363d4b5fb43d), [`crates`](https://gist.github.com/Shnatsel/dc0ec81f6ad392b8967e8d3f2b1f5f80), [`json`](https://gist.github.com/Shnatsel/511ad1f87528c450157ef9ad09984745). 12 | 13 | ## Usage 14 | 15 | To install this tool, please run the following command: 16 | 17 | ```shell 18 | cargo install cargo-supply-chain 19 | ``` 20 | 21 | Then run it with: 22 | 23 | ```shell 24 | cargo supply-chain publishers 25 | ``` 26 | 27 | By default the supply chain is listed for **all targets** and **default features only**. 28 | 29 | You can alter this behavior by passing `--target=…` to list dependencies for a specific target. 30 | You can use `--all-features`, `--no-default-features`, and `--features=…` to control feature selection. 31 | 32 | Here's a list of subcommands: 33 | 34 | ```none 35 | Gather author, contributor and publisher data on crates in your dependency graph 36 | 37 | Usage: COMMAND [ARG]… 38 | 39 | Available options: 40 | -h, --help Prints help information 41 | -v, --version Prints version information 42 | 43 | Available commands: 44 | publishers List all crates.io publishers in the depedency graph 45 | crates List all crates in dependency graph and crates.io publishers for each 46 | json Like 'crates', but in JSON and with more fields for each publisher 47 | update Download the latest daily dump from crates.io to speed up other commands 48 | 49 | Most commands also accept flags controlling the features, targets, etc. 50 | See 'cargo supply-chain --help' for more information on a specific command. 51 | ``` 52 | 53 | ## License 54 | 55 | Triple licensed under any of Apache-2.0, MIT, or zlib terms. 56 | -------------------------------------------------------------------------------- /deps_tests/README.md: -------------------------------------------------------------------------------- 1 | # deps_tests 2 | 3 | The files in this directory are used by tests in `../src/common.rs`. 4 | 5 | Each of the `.metadata.json` files was generated with a command of the following form: 6 | 7 | ```sh 8 | cargo metadata | 9 | sed "s,${PWD},\$CARGO_MANIFEST_DIR,g" | 10 | sed "s,${HOME},\$HOME,g" | 11 | jq --sort-keys > ${CARGO_SUPPLY_CHAIN_DIR}/deps_tests/${PACKAGE}_${VERSION}.metadata.json 12 | ``` 13 | 14 | The other files were then generated with the following command: 15 | 16 | ``` 17 | BLESS=1 cargo test 'tests::deps' 18 | ``` 19 | 20 | Optionally, all of the `.json` files can be normalized with the following command: 21 | 22 | ```sh 23 | for X in *.json; do 24 | Y="$(mktemp)" 25 | jq --sort-keys < "$X" > "$Y" 26 | mv -f "$Y" "$X" 27 | done 28 | ``` 29 | 30 | "Optionally" because the tests should not require the `.json` files to be normalized. 31 | -------------------------------------------------------------------------------- /src/api_client.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | pub struct RateLimitedClient { 4 | last_request_time: Option, 5 | agent: ureq::Agent, 6 | } 7 | 8 | impl Default for RateLimitedClient { 9 | fn default() -> Self { 10 | RateLimitedClient { 11 | last_request_time: None, 12 | agent: ureq::agent(), 13 | } 14 | } 15 | } 16 | 17 | impl RateLimitedClient { 18 | pub fn new() -> Self { 19 | RateLimitedClient::default() 20 | } 21 | 22 | pub fn get(&mut self, url: &str) -> ureq::Request { 23 | self.wait_to_honor_rate_limit(); 24 | self.agent.get(url).set( 25 | "User-Agent", 26 | "cargo supply-chain (https://github.com/rust-secure-code/cargo-supply-chain)", 27 | ) 28 | } 29 | 30 | /// Waits until at least 1 second has elapsed since last request, 31 | /// as per 32 | fn wait_to_honor_rate_limit(&mut self) { 33 | if let Some(prev_req_time) = self.last_request_time { 34 | let next_req_time = prev_req_time + Duration::from_secs(1); 35 | if let Some(time_to_wait) = next_req_time.checked_duration_since(Instant::now()) { 36 | std::thread::sleep(time_to_wait); 37 | } 38 | } 39 | self.last_request_time = Some(Instant::now()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use bpaf::*; 2 | use std::{path::PathBuf, time::Duration}; 3 | 4 | /// Arguments to be passed to `cargo metadata` 5 | #[derive(Clone, Debug, Bpaf)] 6 | #[bpaf(generate(meta_args))] 7 | pub struct MetadataArgs { 8 | // `all_features` and `no_default_features` are not mutually exclusive in `cargo metadata`, 9 | // in the sense that it will not error out when encountering them; it just follows `all_features` 10 | /// Activate all available features 11 | pub all_features: bool, 12 | 13 | /// Do not activate the `default` feature 14 | pub no_default_features: bool, 15 | 16 | /// Ignore dev-only dependencies 17 | pub no_dev: bool, 18 | 19 | // This is a `String` because we don't parse the value, just pass it on to `cargo metadata` blindly 20 | /// Space or comma separated list of features to activate 21 | #[bpaf(argument("FEATURES"))] 22 | pub features: Option, 23 | 24 | /// Only include dependencies matching the given target-triple 25 | #[bpaf(argument("TRIPLE"))] 26 | pub target: Option, 27 | 28 | /// Path to Cargo.toml 29 | #[bpaf(argument("PATH"))] 30 | pub manifest_path: Option, 31 | } 32 | 33 | /// Arguments for typical querying commands - crates, publishers, json 34 | #[derive(Clone, Debug, Bpaf)] 35 | #[bpaf(generate(args))] 36 | pub(crate) struct QueryCommandArgs { 37 | #[bpaf(external)] 38 | pub cache_max_age: Duration, 39 | 40 | /// Make output more friendly towards tools such as `diff` 41 | #[bpaf(short, long)] 42 | pub diffable: bool, 43 | } 44 | 45 | #[derive(Clone, Debug, Bpaf)] 46 | pub(crate) enum PrintJson { 47 | /// Print JSON schema and exit 48 | #[bpaf(long("print-schema"))] 49 | Schema, 50 | 51 | Info { 52 | #[bpaf(external)] 53 | args: QueryCommandArgs, 54 | #[bpaf(external)] 55 | meta_args: MetadataArgs, 56 | }, 57 | } 58 | 59 | /// Gather author, contributor and publisher data on crates in your dependency graph 60 | /// 61 | /// 62 | /// Most commands also accept flags controlling the features, targets, etc. 63 | /// See 'cargo supply-chain --help' for more information on a specific command. 64 | #[derive(Clone, Debug, Bpaf)] 65 | #[bpaf(options("supply-chain"), generate(args_parser), version)] 66 | pub(crate) enum CliArgs { 67 | /// Lists all crates.io publishers in the dependency graph and owned crates for each 68 | /// 69 | /// 70 | /// If a local cache created by 'update' subcommand is present and up to date, 71 | /// it will be used. Otherwise live data will be fetched from the crates.io API. 72 | #[bpaf(command)] 73 | Publishers { 74 | #[bpaf(external)] 75 | args: QueryCommandArgs, 76 | #[bpaf(external)] 77 | meta_args: MetadataArgs, 78 | }, 79 | 80 | /// List all crates in dependency graph and crates.io publishers for each 81 | /// 82 | /// 83 | /// If a local cache created by 'update' subcommand is present and up to date, 84 | /// it will be used. Otherwise live data will be fetched from the crates.io API. 85 | #[bpaf(command)] 86 | Crates { 87 | #[bpaf(external)] 88 | args: QueryCommandArgs, 89 | #[bpaf(external)] 90 | meta_args: MetadataArgs, 91 | }, 92 | 93 | /// Detailed info on publishers of all crates in the dependency graph, in JSON 94 | /// 95 | /// The JSON schema is also available, use --print-schema to get it. 96 | /// 97 | /// If a local cache created by 'update' subcommand is present and up to date, 98 | /// it will be used. Otherwise live data will be fetched from the crates.io API.", 99 | #[bpaf(command)] 100 | Json(#[bpaf(external(print_json))] PrintJson), 101 | 102 | /// Download the latest daily dump from crates.io to speed up other commands 103 | /// 104 | /// 105 | /// If the local cache is already younger than specified in '--cache-max-age' option, 106 | /// a newer version will not be downloaded. 107 | /// 108 | /// Note that this downloads the entire crates.io database, which is hundreds of Mb of data! 109 | /// If you are on a metered connection, you should not be running the 'update' subcommand. 110 | /// Instead, rely on requests to the live API - they are slower, but use much less data. 111 | #[bpaf(command)] 112 | Update { 113 | #[bpaf(external)] 114 | cache_max_age: Duration, 115 | }, 116 | } 117 | 118 | fn cache_max_age() -> impl Parser { 119 | long("cache-max-age") 120 | .help( 121 | "\ 122 | The cache will be considered valid while younger than specified. 123 | The format is a human readable duration such as `1w` or `1d 6h`. 124 | If not specified, the cache is considered valid for 48 hours.", 125 | ) 126 | .argument::("AGE") 127 | .parse(|text| humantime::parse_duration(&text)) 128 | .fallback(Duration::from_secs(48 * 3600)) 129 | } 130 | 131 | #[cfg(test)] 132 | mod tests { 133 | use super::*; 134 | 135 | fn parse_args(args: &[&str]) -> Result { 136 | args_parser().run_inner(Args::from(args)) 137 | } 138 | 139 | #[test] 140 | fn test_cache_max_age_parser() { 141 | let _ = parse_args(&["crates", "--cache-max-age", "7d"]).unwrap(); 142 | let _ = parse_args(&["crates", "--cache-max-age=7d"]).unwrap(); 143 | let _ = parse_args(&["crates", "--cache-max-age=1w"]).unwrap(); 144 | let _ = parse_args(&["crates", "--cache-max-age=1m"]).unwrap(); 145 | let _ = parse_args(&["crates", "--cache-max-age=1s"]).unwrap(); 146 | // erroneous invocations that must be rejected 147 | assert!(parse_args(&["crates", "--cache-max-age"]).is_err()); 148 | assert!(parse_args(&["crates", "--cache-max-age=5"]).is_err()); 149 | } 150 | 151 | #[test] 152 | fn test_accepted_query_options() { 153 | for command in ["crates", "publishers", "json"] { 154 | let _ = args_parser().run_inner(&[command][..]).unwrap(); 155 | let _ = args_parser().run_inner(&[command, "-d"][..]).unwrap(); 156 | let _ = args_parser() 157 | .run_inner(&[command, "--diffable"][..]) 158 | .unwrap(); 159 | let _ = args_parser() 160 | .run_inner(&[command, "--cache-max-age=7d"][..]) 161 | .unwrap(); 162 | let _ = args_parser() 163 | .run_inner(&[command, "-d", "--cache-max-age=7d"][..]) 164 | .unwrap(); 165 | let _ = args_parser() 166 | .run_inner(&[command, "--diffable", "--cache-max-age=7d"][..]) 167 | .unwrap(); 168 | } 169 | } 170 | 171 | #[test] 172 | fn test_accepted_update_options() { 173 | let _ = args_parser().run_inner(Args::from(&["update"])).unwrap(); 174 | let _ = parse_args(&["update", "--cache-max-age=7d"]).unwrap(); 175 | // erroneous invocations that must be rejected 176 | assert!(parse_args(&["update", "-d"]).is_err()); 177 | assert!(parse_args(&["update", "--diffable"]).is_err()); 178 | assert!(parse_args(&["update", "-d", "--cache-max-age=7d"]).is_err()); 179 | assert!(parse_args(&["update", "--diffable", "--cache-max-age=7d"]).is_err()); 180 | } 181 | 182 | #[test] 183 | fn test_json_schema_option() { 184 | let _ = parse_args(&["json", "--print-schema"]).unwrap(); 185 | // erroneous invocations that must be rejected 186 | assert!(parse_args(&["json", "--print-schema", "-d"]).is_err()); 187 | assert!(parse_args(&["json", "--print-schema", "--diffable"]).is_err()); 188 | assert!(parse_args(&["json", "--print-schema", "--cache-max-age=7d"]).is_err()); 189 | assert!( 190 | parse_args(&["json", "--print-schema", "--diffable", "--cache-max-age=7d"]).is_err() 191 | ); 192 | } 193 | 194 | #[test] 195 | fn test_invocation_through_cargo() { 196 | let _ = parse_args(&["supply-chain", "update"]).unwrap(); 197 | let _ = parse_args(&["supply-chain", "publishers", "-d"]).unwrap(); 198 | let _ = parse_args(&["supply-chain", "crates", "-d", "--cache-max-age=5h"]).unwrap(); 199 | let _ = parse_args(&["supply-chain", "json", "--diffable"]).unwrap(); 200 | let _ = parse_args(&["supply-chain", "json", "--print-schema"]).unwrap(); 201 | // erroneous invocations to be rejected 202 | assert!(parse_args(&["supply-chain", "supply-chain", "json", "--print-schema"]).is_err()); 203 | assert!(parse_args(&["supply-chain", "supply-chain", "crates", "-d"]).is_err()); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/common.rs: -------------------------------------------------------------------------------- 1 | use anyhow::bail; 2 | use cargo_metadata::{ 3 | semver::VersionReq, CargoOpt::AllFeatures, CargoOpt::NoDefaultFeatures, Dependency, 4 | DependencyKind, Metadata, MetadataCommand, Package, PackageId, 5 | }; 6 | use std::collections::{HashMap, HashSet}; 7 | 8 | pub use crate::cli::MetadataArgs; 9 | 10 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 11 | #[cfg_attr(test, derive(serde::Deserialize, serde::Serialize))] 12 | pub enum PkgSource { 13 | Local, 14 | CratesIo, 15 | Foreign, 16 | } 17 | 18 | #[derive(Debug, Clone)] 19 | #[cfg_attr(test, derive(Eq, PartialEq, serde::Deserialize, serde::Serialize))] 20 | pub struct SourcedPackage { 21 | pub source: PkgSource, 22 | pub package: Package, 23 | } 24 | 25 | fn metadata_command(args: MetadataArgs) -> MetadataCommand { 26 | let mut command = MetadataCommand::new(); 27 | if args.all_features { 28 | command.features(AllFeatures); 29 | } 30 | if args.no_default_features { 31 | command.features(NoDefaultFeatures); 32 | } 33 | if let Some(path) = args.manifest_path { 34 | command.manifest_path(path); 35 | } 36 | let mut other_options = Vec::new(); 37 | if let Some(target) = args.target { 38 | other_options.push(format!("--filter-platform={}", target)); 39 | } 40 | // `cargo-metadata` crate assumes we have a Vec of features, 41 | // but we really didn't want to parse it ourselves, so we pass the argument directly 42 | if let Some(features) = args.features { 43 | other_options.push(format!("--features={}", features)); 44 | } 45 | command.other_options(other_options); 46 | command 47 | } 48 | 49 | pub fn sourced_dependencies( 50 | metadata_args: MetadataArgs, 51 | ) -> Result, anyhow::Error> { 52 | let no_dev = metadata_args.no_dev; 53 | let command = metadata_command(metadata_args); 54 | let meta = match command.exec() { 55 | Ok(v) => v, 56 | Err(cargo_metadata::Error::CargoMetadata { stderr: e }) => bail!(e), 57 | Err(err) => bail!("Failed to fetch crate metadata!\n {}", err), 58 | }; 59 | 60 | sourced_dependencies_from_metadata(meta, no_dev) 61 | } 62 | 63 | fn sourced_dependencies_from_metadata( 64 | meta: Metadata, 65 | no_dev: bool, 66 | ) -> Result, anyhow::Error> { 67 | let mut how: HashMap = HashMap::new(); 68 | let mut what: HashMap = meta 69 | .packages 70 | .iter() 71 | .map(|package| (package.id.clone(), package.clone())) 72 | .collect(); 73 | 74 | for pkg in &meta.packages { 75 | // Suppose every package is foreign, until proven otherwise.. 76 | how.insert(pkg.id.clone(), PkgSource::Foreign); 77 | } 78 | 79 | // Find the crates.io dependencies.. 80 | for pkg in &meta.packages { 81 | if let Some(source) = pkg.source.as_ref() { 82 | if source.is_crates_io() { 83 | how.insert(pkg.id.clone(), PkgSource::CratesIo); 84 | } 85 | } 86 | } 87 | 88 | for pkg in meta.workspace_members { 89 | *how.get_mut(&pkg).unwrap() = PkgSource::Local; 90 | } 91 | 92 | if no_dev { 93 | (how, what) = extract_non_dev_dependencies(&mut how, &mut what); 94 | } 95 | 96 | let dependencies: Vec<_> = how 97 | .iter() 98 | .map(|(id, kind)| { 99 | let dep = what.get(id).cloned().unwrap(); 100 | SourcedPackage { 101 | source: *kind, 102 | package: dep, 103 | } 104 | }) 105 | .collect(); 106 | 107 | Ok(dependencies) 108 | } 109 | 110 | #[derive(Eq, Hash, PartialEq)] 111 | struct Dep { 112 | name: String, 113 | req: VersionReq, 114 | } 115 | 116 | impl Dep { 117 | fn from_cargo_metadata_dependency(dep: &Dependency) -> Self { 118 | Self { 119 | name: dep.name.clone(), 120 | req: dep.req.clone(), 121 | } 122 | } 123 | 124 | fn matches(&self, pkg: &Package) -> bool { 125 | self.name == pkg.name && self.req.matches(&pkg.version) 126 | } 127 | } 128 | 129 | /// Start with the `PkgSource::Local` packages, then iteratively add non-dev-dependencies until no more 130 | /// packages can be added, and return the results. 131 | /// 132 | /// Note that matching dependencies to packages is "best effort." The fields that Cargo uses to 133 | /// determine a package's id are its name, version, and source: 134 | /// https://github.com/rust-lang/cargo/blob/dd5134c7a59e3a3b8587f1ef04a930185d2ca503/src/cargo/core/package_id.rs#L29-L31 135 | /// 136 | /// When matching dependencies to packages, we use the package's name and version, but not its source 137 | /// (see [`Dep`]). Experiments suggest that source strings can vary. So comparing them seems risky. 138 | /// Also, it is better to err on the side of inclusion. 139 | fn extract_non_dev_dependencies( 140 | how: &mut HashMap, 141 | what: &mut HashMap, 142 | ) -> (HashMap, HashMap) { 143 | let mut how_new = HashMap::new(); 144 | let mut what_new = HashMap::new(); 145 | 146 | let mut ids = how 147 | .iter() 148 | .filter_map(|(id, source)| { 149 | if matches!(source, PkgSource::Local) { 150 | Some(id.clone()) 151 | } else { 152 | None 153 | } 154 | }) 155 | .collect::>(); 156 | 157 | while !ids.is_empty() { 158 | let mut deps = HashSet::new(); 159 | 160 | for id in ids.drain(..) { 161 | for dep in &what.get(&id).unwrap().dependencies { 162 | if dep.kind != DependencyKind::Development { 163 | deps.insert(Dep::from_cargo_metadata_dependency(dep)); 164 | } 165 | } 166 | 167 | how_new.insert(id.clone(), how.remove(&id).unwrap()); 168 | what_new.insert(id.clone(), what.remove(&id).unwrap()); 169 | } 170 | 171 | for pkg in what.values() { 172 | if deps.iter().any(|dep| dep.matches(pkg)) { 173 | ids.push(pkg.id.clone()); 174 | } 175 | } 176 | } 177 | 178 | (how_new, what_new) 179 | } 180 | 181 | pub fn crate_names_from_source(crates: &[SourcedPackage], source: PkgSource) -> Vec { 182 | let mut filtered_crate_names: Vec = crates 183 | .iter() 184 | .filter(|p| p.source == source) 185 | .map(|p| p.package.name.clone()) 186 | .collect(); 187 | // Collecting into a HashSet is less user-friendly because order varies between runs 188 | filtered_crate_names.sort_unstable(); 189 | filtered_crate_names.dedup(); 190 | filtered_crate_names 191 | } 192 | 193 | pub fn complain_about_non_crates_io_crates(dependencies: &[SourcedPackage]) { 194 | { 195 | // scope bound to avoid accidentally referencing local crates when working with foreign ones 196 | let local_crate_names = crate_names_from_source(dependencies, PkgSource::Local); 197 | if !local_crate_names.is_empty() { 198 | eprintln!( 199 | "\nThe following crates will be ignored because they come from a local directory:" 200 | ); 201 | for crate_name in &local_crate_names { 202 | eprintln!(" - {}", crate_name); 203 | } 204 | } 205 | } 206 | 207 | { 208 | let foreign_crate_names = crate_names_from_source(dependencies, PkgSource::Foreign); 209 | if !foreign_crate_names.is_empty() { 210 | eprintln!("\nCannot audit the following crates because they are not from crates.io:"); 211 | for crate_name in &foreign_crate_names { 212 | eprintln!(" - {}", crate_name); 213 | } 214 | } 215 | } 216 | } 217 | 218 | pub fn comma_separated_list(list: &[String]) -> String { 219 | let mut result = String::new(); 220 | let mut first_loop = true; 221 | for crate_name in list { 222 | if !first_loop { 223 | result.push_str(", "); 224 | } 225 | first_loop = false; 226 | result.push_str(crate_name.as_str()); 227 | } 228 | result 229 | } 230 | 231 | #[cfg(test)] 232 | mod tests { 233 | use super::{sourced_dependencies_from_metadata, SourcedPackage}; 234 | use cargo_metadata::Metadata; 235 | use std::{ 236 | cmp::Ordering, 237 | env::var, 238 | fs::{read_dir, read_to_string, write}, 239 | path::Path, 240 | }; 241 | 242 | #[test] 243 | fn deps() { 244 | for entry in read_dir("deps_tests").unwrap() { 245 | let entry = entry.unwrap(); 246 | let path = entry.path(); 247 | 248 | let Some(prefix) = path 249 | .to_string_lossy() 250 | .strip_suffix(".metadata.json") 251 | .map(ToOwned::to_owned) 252 | else { 253 | continue; 254 | }; 255 | 256 | let contents = read_to_string(&path).unwrap(); 257 | 258 | // Help ensure private information is not leaked. 259 | assert!(var("HOME").map_or(true, |home| !contents.contains(&home))); 260 | 261 | let metadata = serde_json::from_str::(&contents).unwrap(); 262 | 263 | for no_dev in [false, true] { 264 | let path = prefix.clone() + ".deps" + if no_dev { "_no_dev" } else { "" } + ".json"; 265 | 266 | let mut deps_from_metadata = 267 | sourced_dependencies_from_metadata(metadata.clone(), no_dev).unwrap(); 268 | deps_from_metadata.sort_by(cmp_dep); 269 | 270 | if enabled("BLESS") { 271 | let contents = serde_json::to_string_pretty(&deps_from_metadata).unwrap(); 272 | write(path, &contents).unwrap(); 273 | continue; 274 | } 275 | 276 | let mut deps_from_file = sourced_dependencies_from_file(&path); 277 | deps_from_file.sort_by(cmp_dep); 278 | 279 | assert_eq!(deps_from_file, deps_from_metadata); 280 | } 281 | } 282 | } 283 | 284 | // `cargo` has `snapbox` as a dev dependency. `snapbox` has `snapbox-macros` as a normal dependency. 285 | 286 | #[test] 287 | fn cargo() { 288 | let deps = sourced_dependencies_from_file("deps_tests/cargo_0.70.1.deps.json"); 289 | 290 | assert!(deps.iter().any(|dep| dep.package.name == "snapbox")); 291 | assert!(deps.iter().any(|dep| dep.package.name == "snapbox-macros")); 292 | } 293 | 294 | #[test] 295 | fn cargo_no_dev() { 296 | let deps = sourced_dependencies_from_file("deps_tests/cargo_0.70.1.deps_no_dev.json"); 297 | 298 | assert!(deps.iter().all(|dep| dep.package.name != "snapbox")); 299 | assert!(deps.iter().all(|dep| dep.package.name != "snapbox-macros")); 300 | } 301 | 302 | #[test] 303 | fn snapbox() { 304 | let deps = sourced_dependencies_from_file("deps_tests/snapbox_0.4.11.deps.json"); 305 | 306 | assert!(deps.iter().any(|dep| dep.package.name == "snapbox-macros")); 307 | } 308 | 309 | fn sourced_dependencies_from_file(path: impl AsRef) -> Vec { 310 | let contents = read_to_string(path).unwrap(); 311 | serde_json::from_str::>(&contents).unwrap() 312 | } 313 | 314 | fn cmp_dep(left: &SourcedPackage, right: &SourcedPackage) -> Ordering { 315 | left.package.id.cmp(&right.package.id) 316 | } 317 | 318 | fn enabled(key: &str) -> bool { 319 | var(key).map_or(false, |value| value != "0") 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/crates_cache.rs: -------------------------------------------------------------------------------- 1 | use crate::api_client::RateLimitedClient; 2 | use crate::publishers::{PublisherData, PublisherKind}; 3 | use flate2::read::GzDecoder; 4 | use serde::{Deserialize, Serialize}; 5 | use std::{ 6 | collections::{BTreeSet, HashMap}, 7 | fs, 8 | io::{self, ErrorKind}, 9 | path::PathBuf, 10 | time::Duration, 11 | time::SystemTimeError, 12 | }; 13 | 14 | pub struct CratesCache { 15 | cache_dir: Option, 16 | metadata: Option, 17 | crates: Option>, 18 | crate_owners: Option>>, 19 | users: Option>, 20 | teams: Option>, 21 | versions: Option>, 22 | } 23 | 24 | pub enum CacheState { 25 | Fresh, 26 | Expired, 27 | Unknown, 28 | } 29 | 30 | pub enum DownloadState { 31 | /// The tag still matched and resource was not stale. 32 | Fresh, 33 | /// There was a newer resource. 34 | Expired, 35 | /// We forced the download of an update. 36 | Stale, 37 | } 38 | 39 | struct CacheDir(PathBuf); 40 | 41 | #[derive(Clone, Deserialize, Serialize)] 42 | struct Metadata { 43 | #[serde(with = "humantime_serde")] 44 | timestamp: std::time::SystemTime, 45 | } 46 | 47 | #[derive(Clone, Deserialize, Serialize)] 48 | struct MetadataStored { 49 | #[serde(with = "humantime_serde")] 50 | timestamp: std::time::SystemTime, 51 | #[serde(default)] 52 | etag: Option, 53 | } 54 | 55 | #[derive(Clone, Deserialize, Serialize)] 56 | struct Crate { 57 | name: String, 58 | id: u64, 59 | repository: Option, 60 | } 61 | 62 | #[derive(Clone, Deserialize, Serialize)] 63 | struct CrateOwner { 64 | crate_id: u64, 65 | owner_id: u64, 66 | owner_kind: i32, 67 | } 68 | 69 | #[derive(Clone, Deserialize, Serialize)] 70 | struct Publisher { 71 | crate_id: u64, 72 | published_by: u64, 73 | } 74 | 75 | #[derive(Clone, Deserialize, Serialize)] 76 | struct Team { 77 | id: u64, 78 | avatar: Option, 79 | login: String, 80 | name: Option, 81 | } 82 | 83 | #[derive(Clone, Deserialize, Serialize)] 84 | struct User { 85 | id: u64, 86 | gh_avatar: Option, 87 | gh_id: Option, 88 | gh_login: String, 89 | name: Option, 90 | } 91 | 92 | impl CratesCache { 93 | const METADATA_FS: &'static str = "metadata.json"; 94 | const CRATES_FS: &'static str = "crates.json"; 95 | const CRATE_OWNERS_FS: &'static str = "crate_owners.json"; 96 | const USERS_FS: &'static str = "users.json"; 97 | const TEAMS_FS: &'static str = "teams.json"; 98 | const VERSIONS_FS: &'static str = "versions.json"; 99 | 100 | const DUMP_URL: &'static str = "https://static.crates.io/db-dump.tar.gz"; 101 | 102 | /// Open a crates cache. 103 | pub fn new() -> Self { 104 | CratesCache { 105 | cache_dir: Self::cache_dir().map(CacheDir), 106 | metadata: None, 107 | crates: None, 108 | crate_owners: None, 109 | users: None, 110 | teams: None, 111 | versions: None, 112 | } 113 | } 114 | 115 | fn cache_dir() -> Option { 116 | xdg::BaseDirectories::with_prefix("cargo-supply-chain") 117 | .ok() 118 | .map(|base_directories| base_directories.get_cache_home()) 119 | } 120 | 121 | /// Re-download the list from the data dumps. 122 | pub fn download( 123 | &mut self, 124 | client: &mut RateLimitedClient, 125 | max_age: Duration, 126 | ) -> Result { 127 | let bar = indicatif::ProgressBar::new(!0) 128 | .with_prefix("Downloading") 129 | .with_style( 130 | indicatif::ProgressStyle::default_spinner() 131 | .template("{prefix:>12.bright.cyan} {spinner} {msg:.cyan}") 132 | .unwrap(), 133 | ) 134 | .with_message("preparing"); 135 | 136 | let remembered_etag; 137 | let response = { 138 | let mut request = client.get(Self::DUMP_URL); 139 | if let Some(meta) = self.load_metadata() { 140 | remembered_etag = meta.etag.clone(); 141 | // See if we can consider the resource not-yet-stale. 142 | if meta.validate(max_age) == Some(true) { 143 | if let Some(etag) = meta.etag.as_ref() { 144 | request = request.set("if-none-match", etag); 145 | } 146 | } 147 | } else { 148 | remembered_etag = None; 149 | } 150 | request.call() 151 | } 152 | .map_err(|e| io::Error::new(ErrorKind::Other, e))?; 153 | 154 | // Not modified. 155 | if response.status() == 304 { 156 | bar.finish_and_clear(); 157 | return Ok(DownloadState::Fresh); 158 | } 159 | 160 | if let Some(length) = response 161 | .header("content-length") 162 | .and_then(|l| l.parse().ok()) 163 | { 164 | bar.set_style( 165 | indicatif::ProgressStyle::default_bar() 166 | .template("{prefix:>12.bright.cyan} [{bar:27}] {bytes:>9}/{total_bytes:9} {bytes_per_sec} ETA {eta:4} - {msg:.cyan}").unwrap() 167 | .progress_chars("=> ")); 168 | bar.set_length(length); 169 | } else { 170 | bar.println("Length unspecified, expect at least 250MiB"); 171 | bar.set_style(indicatif::ProgressStyle::default_spinner().template( 172 | "{prefix:>12.bright.cyan} {spinner} {bytes:>9} {bytes_per_sec} - {msg:.cyan}", 173 | ).unwrap()); 174 | } 175 | 176 | let etag = response.header("etag").map(String::from); 177 | let reader = bar.wrap_read(response.into_reader()); 178 | let ungzip = GzDecoder::new(reader); 179 | let mut archive = tar::Archive::new(ungzip); 180 | 181 | let cache_dir = CratesCache::cache_dir().ok_or(ErrorKind::NotFound)?; 182 | let mut cache_updater = CacheUpdater::new(cache_dir)?; 183 | let required_files = [ 184 | Self::CRATE_OWNERS_FS, 185 | Self::CRATES_FS, 186 | Self::USERS_FS, 187 | Self::TEAMS_FS, 188 | Self::METADATA_FS, 189 | ] 190 | .iter() 191 | .map(ToString::to_string) 192 | .collect::>(); 193 | 194 | for entry in (archive.entries()?).flatten() { 195 | if let Ok(path) = entry.path() { 196 | if let Some(name) = path.file_name().and_then(std::ffi::OsStr::to_str) { 197 | bar.set_message(name.to_string()); 198 | } 199 | } 200 | if entry.path_bytes().ends_with(b"crate_owners.csv") { 201 | let owners: Vec = read_csv_data(entry)?; 202 | cache_updater.store_multi_map( 203 | &mut self.crate_owners, 204 | Self::CRATE_OWNERS_FS, 205 | owners.as_slice(), 206 | &|owner| owner.crate_id, 207 | )?; 208 | } else if entry.path_bytes().ends_with(b"crates.csv") { 209 | let crates: Vec = read_csv_data(entry)?; 210 | cache_updater.store_map( 211 | &mut self.crates, 212 | Self::CRATES_FS, 213 | crates.as_slice(), 214 | &|crate_| crate_.name.clone(), 215 | )?; 216 | } else if entry.path_bytes().ends_with(b"users.csv") { 217 | let users: Vec = read_csv_data(entry)?; 218 | cache_updater.store_map( 219 | &mut self.users, 220 | Self::USERS_FS, 221 | users.as_slice(), 222 | &|user| user.id, 223 | )?; 224 | } else if entry.path_bytes().ends_with(b"teams.csv") { 225 | let teams: Vec = read_csv_data(entry)?; 226 | cache_updater.store_map( 227 | &mut self.teams, 228 | Self::TEAMS_FS, 229 | teams.as_slice(), 230 | &|team| team.id, 231 | )?; 232 | } else if entry.path_bytes().ends_with(b"metadata.json") { 233 | let meta: Metadata = serde_json::from_reader(entry)?; 234 | cache_updater.store( 235 | &mut self.metadata, 236 | Self::METADATA_FS, 237 | MetadataStored { 238 | timestamp: meta.timestamp, 239 | etag: etag.clone(), 240 | }, 241 | )?; 242 | } else { 243 | // This was not a file with a filename we actually use. 244 | // Check if we've obtained all the files we need. 245 | // If yes, we can end the download early. 246 | // This saves hundreds of megabytes of traffic. 247 | if required_files.is_subset(&cache_updater.staged_files) { 248 | break; 249 | } 250 | } 251 | } 252 | // Now that we've successfully downloaded and stored everything, 253 | // replace the old cache contents with the new one. 254 | cache_updater.commit()?; 255 | 256 | // If we get here, we had no etag or the etag mismatched or we forced a download due to 257 | // stale data. Catch the last as it means the crates.io daily dumps were not updated. 258 | if remembered_etag == etag { 259 | Ok(DownloadState::Stale) 260 | } else { 261 | Ok(DownloadState::Expired) 262 | } 263 | } 264 | 265 | pub fn expire(&mut self, max_age: Duration) -> CacheState { 266 | match self.validate(max_age) { 267 | // Still fresh. 268 | Some(true) => CacheState::Fresh, 269 | // There was no valid meta data. Consider expired for safety. 270 | None => { 271 | self.cache_dir = None; 272 | CacheState::Unknown 273 | } 274 | Some(false) => { 275 | self.cache_dir = None; 276 | CacheState::Expired 277 | } 278 | } 279 | } 280 | 281 | pub fn age(&mut self) -> Option { 282 | match self.load_metadata() { 283 | Some(meta) => meta.age().ok(), 284 | None => None, 285 | } 286 | } 287 | 288 | pub fn publisher_users(&mut self, crate_name: &str) -> Option> { 289 | let id = self.load_crates()?.get(crate_name)?.id; 290 | let owners = self.load_crate_owners()?.get(&id)?.clone(); 291 | let users = self.load_users()?; 292 | let publisher = owners 293 | .into_iter() 294 | .filter(|owner| owner.owner_kind == 0) 295 | .filter_map(|owner: CrateOwner| { 296 | let user = users.get(&owner.owner_id)?; 297 | Some(PublisherData { 298 | id: user.id, 299 | avatar: user.gh_avatar.clone(), 300 | login: user.gh_login.clone(), 301 | name: user.name.clone(), 302 | kind: PublisherKind::user, 303 | }) 304 | }) 305 | .collect(); 306 | Some(publisher) 307 | } 308 | 309 | pub fn publisher_teams(&mut self, crate_name: &str) -> Option> { 310 | let id = self.load_crates()?.get(crate_name)?.id; 311 | let owners = self.load_crate_owners()?.get(&id)?.clone(); 312 | let teams = self.load_teams()?; 313 | let publisher = owners 314 | .into_iter() 315 | .filter(|owner| owner.owner_kind == 1) 316 | .filter_map(|owner: CrateOwner| { 317 | let team = teams.get(&owner.owner_id)?; 318 | Some(PublisherData { 319 | id: team.id, 320 | avatar: team.avatar.clone(), 321 | login: team.login.clone(), 322 | name: team.name.clone(), 323 | kind: PublisherKind::team, 324 | }) 325 | }) 326 | .collect(); 327 | Some(publisher) 328 | } 329 | 330 | fn validate(&mut self, max_age: Duration) -> Option { 331 | let meta = self.load_metadata()?; 332 | meta.validate(max_age) 333 | } 334 | 335 | fn load_metadata(&mut self) -> Option<&MetadataStored> { 336 | self.cache_dir 337 | .as_ref()? 338 | .load_cached(&mut self.metadata, Self::METADATA_FS) 339 | .ok() 340 | } 341 | 342 | fn load_crates(&mut self) -> Option<&HashMap> { 343 | self.cache_dir 344 | .as_ref()? 345 | .load_cached(&mut self.crates, Self::CRATES_FS) 346 | .ok() 347 | } 348 | 349 | fn load_crate_owners(&mut self) -> Option<&HashMap>> { 350 | self.cache_dir 351 | .as_ref()? 352 | .load_cached(&mut self.crate_owners, Self::CRATE_OWNERS_FS) 353 | .ok() 354 | } 355 | 356 | fn load_users(&mut self) -> Option<&HashMap> { 357 | self.cache_dir 358 | .as_ref()? 359 | .load_cached(&mut self.users, Self::USERS_FS) 360 | .ok() 361 | } 362 | 363 | fn load_teams(&mut self) -> Option<&HashMap> { 364 | self.cache_dir 365 | .as_ref()? 366 | .load_cached(&mut self.teams, Self::TEAMS_FS) 367 | .ok() 368 | } 369 | 370 | fn load_versions(&mut self) -> Option<&HashMap<(u64, String), Publisher>> { 371 | self.cache_dir 372 | .as_ref()? 373 | .load_cached(&mut self.versions, Self::VERSIONS_FS) 374 | .ok() 375 | } 376 | } 377 | 378 | fn read_csv_data( 379 | from: impl io::Read, 380 | ) -> Result, csv::Error> { 381 | let mut reader = csv::ReaderBuilder::new() 382 | .delimiter(b',') 383 | .double_quote(true) 384 | .quoting(true) 385 | .from_reader(from); 386 | reader.deserialize().collect() 387 | } 388 | 389 | impl MetadataStored { 390 | fn validate(&self, max_age: Duration) -> Option { 391 | match self.age() { 392 | Ok(duration) => Some(duration < max_age), 393 | Err(_) => None, 394 | } 395 | } 396 | 397 | pub fn age(&self) -> Result { 398 | self.timestamp.elapsed() 399 | } 400 | } 401 | 402 | impl CacheDir { 403 | fn load_cached<'cache, T>( 404 | &self, 405 | cache: &'cache mut Option, 406 | file: &str, 407 | ) -> Result<&'cache T, io::Error> 408 | where 409 | T: serde::de::DeserializeOwned, 410 | { 411 | match cache { 412 | Some(datum) => Ok(datum), 413 | None => { 414 | let file = fs::File::open(self.0.join(file))?; 415 | let reader = io::BufReader::new(file); 416 | let crates: T = serde_json::from_reader(reader).unwrap(); 417 | Ok(cache.get_or_insert(crates)) 418 | } 419 | } 420 | } 421 | } 422 | 423 | /// Implements a two-phase transactional update mechanism: 424 | /// you can store data, but it will not overwrite previous data until you call `commit()` 425 | struct CacheUpdater { 426 | dir: PathBuf, 427 | staged_files: BTreeSet, 428 | } 429 | 430 | /// Creates the cache directory if it doesn't exist. 431 | /// Returns an error if creation fails. 432 | impl CacheUpdater { 433 | fn new(dir: PathBuf) -> Result { 434 | if !dir.exists() { 435 | fs::create_dir_all(&dir)?; 436 | } 437 | 438 | if !dir.is_dir() { 439 | // Well. We certainly don't want to delete anything. 440 | return Err(io::ErrorKind::AlreadyExists.into()); 441 | } 442 | 443 | Ok(Self { 444 | dir, 445 | staged_files: BTreeSet::new(), 446 | }) 447 | } 448 | 449 | /// Commits to disk any changes that you have staged via the `store()` function. 450 | fn commit(&mut self) -> io::Result<()> { 451 | let mut uncommitted_files = std::mem::take(&mut self.staged_files); 452 | let metadata_file = uncommitted_files.take(CratesCache::METADATA_FS); 453 | for file in uncommitted_files { 454 | let source = self.dir.join(&file).with_extension("part"); 455 | let destination = self.dir.join(&file); 456 | fs::rename(source, destination)?; 457 | } 458 | // metadata_file is special since it contains the timestamp for the cache. 459 | // We will only commit it and update the timestamp if updating everything else succeeds. 460 | // Otherwise it would be possible to create a partially updated cache that's considered fresh. 461 | if let Some(file) = metadata_file { 462 | let source = self.dir.join(&file).with_extension("part"); 463 | let destination = self.dir.join(&file); 464 | fs::rename(source, destination)?; 465 | } 466 | Ok(()) 467 | } 468 | 469 | /// Does not overwrite existing data until `commit()` is called. 470 | /// If you do not call `commit()` after this, the on-disk cache will not be actually updated! 471 | fn store(&mut self, cache: &mut Option, file: &str, value: T) -> Result<(), io::Error> 472 | where 473 | T: Serialize, 474 | { 475 | *cache = None; 476 | let value = cache.get_or_insert(value); 477 | 478 | self.staged_files.insert(file.to_owned()); 479 | let out_path = self.dir.join(file).with_extension("part"); 480 | let out_file = fs::File::create(out_path)?; 481 | let out = io::BufWriter::new(out_file); 482 | serde_json::to_writer(out, value)?; 483 | Ok(()) 484 | } 485 | 486 | fn store_map( 487 | &mut self, 488 | cache: &mut Option>, 489 | file: &str, 490 | entries: &[T], 491 | key_fn: &dyn Fn(&T) -> K, 492 | ) -> Result<(), io::Error> 493 | where 494 | T: Serialize + Clone, 495 | K: Serialize + Eq + std::hash::Hash, 496 | { 497 | let hashed: HashMap = entries 498 | .iter() 499 | .map(|entry| (key_fn(entry), entry.clone())) 500 | .collect(); 501 | self.store(cache, file, hashed) 502 | } 503 | 504 | fn store_multi_map( 505 | &mut self, 506 | cache: &mut Option>>, 507 | file: &str, 508 | entries: &[T], 509 | key_fn: &dyn Fn(&T) -> K, 510 | ) -> Result<(), io::Error> 511 | where 512 | T: Serialize + Clone, 513 | K: Serialize + Eq + std::hash::Hash, 514 | { 515 | let mut hashed: HashMap = HashMap::new(); 516 | for entry in entries.iter() { 517 | let key = key_fn(entry); 518 | hashed 519 | .entry(key) 520 | .or_insert_with(Vec::new) 521 | .push(entry.clone()); 522 | } 523 | self.store(cache, file, hashed) 524 | } 525 | } 526 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! Gather author, contributor, publisher data on crates in your dependency graph. 2 | //! 3 | //! There are some use cases: 4 | //! 5 | //! * Find people and groups worth supporting. 6 | //! * An analysis of all the contributors you implicitly trust by building their software. This 7 | //! might have both a sobering and humbling effect. 8 | //! * Identify risks in your dependency graph. 9 | 10 | #![forbid(unsafe_code)] 11 | 12 | mod api_client; 13 | mod cli; 14 | mod common; 15 | mod crates_cache; 16 | mod publishers; 17 | mod subcommands; 18 | 19 | use cli::CliArgs; 20 | use common::MetadataArgs; 21 | 22 | fn main() -> Result<(), anyhow::Error> { 23 | let args = cli::args_parser().fallback_to_usage().run(); 24 | dispatch_command(args) 25 | } 26 | 27 | fn dispatch_command(args: CliArgs) -> Result<(), anyhow::Error> { 28 | match args { 29 | CliArgs::Publishers { args, meta_args } => { 30 | subcommands::publishers(meta_args, args.diffable, args.cache_max_age)?; 31 | } 32 | CliArgs::Crates { args, meta_args } => { 33 | subcommands::crates(meta_args, args.diffable, args.cache_max_age)?; 34 | } 35 | CliArgs::Update { cache_max_age } => subcommands::update(cache_max_age)?, 36 | CliArgs::Json(json) => match json { 37 | cli::PrintJson::Schema => subcommands::print_schema()?, 38 | cli::PrintJson::Info { args, meta_args } => { 39 | subcommands::json(meta_args, args.diffable, args.cache_max_age)?; 40 | } 41 | }, 42 | } 43 | 44 | Ok(()) 45 | } 46 | -------------------------------------------------------------------------------- /src/publishers.rs: -------------------------------------------------------------------------------- 1 | use crate::api_client::RateLimitedClient; 2 | use crate::crates_cache::{CacheState, CratesCache}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::{ 5 | collections::BTreeMap, 6 | io::{self, ErrorKind}, 7 | time::Duration, 8 | }; 9 | 10 | #[cfg(test)] 11 | use schemars::JsonSchema; 12 | 13 | use crate::common::{crate_names_from_source, PkgSource, SourcedPackage}; 14 | 15 | #[derive(Deserialize)] 16 | struct UsersResponse { 17 | users: Vec, 18 | } 19 | 20 | #[derive(Deserialize)] 21 | struct TeamsResponse { 22 | teams: Vec, 23 | } 24 | 25 | /// Data about a single publisher received from a crates.io API endpoint 26 | #[cfg_attr(test, derive(JsonSchema))] 27 | #[derive(Serialize, Deserialize, Debug, Clone)] 28 | pub struct PublisherData { 29 | pub id: u64, 30 | pub login: String, 31 | pub kind: PublisherKind, 32 | // URL is disabled because it's present in API responses but not in DB dumps, 33 | // so the output would vary inconsistent depending on data source 34 | //pub url: Option, 35 | /// Display name. It is NOT guaranteed to be unique! 36 | pub name: Option, 37 | /// Avatar image URL 38 | pub avatar: Option, 39 | } 40 | 41 | impl PartialEq for PublisherData { 42 | fn eq(&self, other: &Self) -> bool { 43 | self.id == other.id 44 | } 45 | } 46 | 47 | impl Eq for PublisherData { 48 | // holds for PublisherData because we're comparing u64 IDs, and it holds for u64 49 | fn assert_receiver_is_total_eq(&self) {} 50 | } 51 | 52 | impl PartialOrd for PublisherData { 53 | fn partial_cmp(&self, other: &Self) -> Option { 54 | Some(self.id.cmp(&other.id)) 55 | } 56 | } 57 | 58 | impl Ord for PublisherData { 59 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 60 | self.id.cmp(&other.id) 61 | } 62 | } 63 | 64 | #[cfg_attr(test, derive(JsonSchema))] 65 | #[derive(Serialize, Deserialize, Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] 66 | #[allow(non_camel_case_types)] 67 | pub enum PublisherKind { 68 | team, 69 | user, 70 | } 71 | 72 | pub fn publisher_users( 73 | client: &mut RateLimitedClient, 74 | crate_name: &str, 75 | ) -> Result, io::Error> { 76 | let url = format!("https://crates.io/api/v1/crates/{}/owner_user", crate_name); 77 | let resp = get_with_retry(&url, client, 3)?; 78 | let data: UsersResponse = resp.into_json()?; 79 | Ok(data.users) 80 | } 81 | 82 | pub fn publisher_teams( 83 | client: &mut RateLimitedClient, 84 | crate_name: &str, 85 | ) -> Result, io::Error> { 86 | let url = format!("https://crates.io/api/v1/crates/{}/owner_team", crate_name); 87 | let resp = get_with_retry(&url, client, 3)?; 88 | let data: TeamsResponse = resp.into_json()?; 89 | Ok(data.teams) 90 | } 91 | 92 | fn get_with_retry( 93 | url: &str, 94 | client: &mut RateLimitedClient, 95 | attempts: u8, 96 | ) -> Result { 97 | let mut resp = client 98 | .get(url) 99 | .call() 100 | .map_err(|e| io::Error::new(ErrorKind::Other, e))?; 101 | 102 | let mut count = 1; 103 | let mut wait = 5; 104 | while resp.status() != 200 && count <= attempts { 105 | eprintln!( 106 | "Failed retrieving {:?}, trying again in {} seconds, attempt {}/{}", 107 | url, wait, count, attempts 108 | ); 109 | std::thread::sleep(std::time::Duration::from_secs(wait)); 110 | 111 | resp = client 112 | .get(url) 113 | .call() 114 | .map_err(|e| io::Error::new(ErrorKind::Other, e))?; 115 | 116 | count += 1; 117 | wait *= 3; 118 | } 119 | 120 | Ok(resp) 121 | } 122 | 123 | pub fn fetch_owners_of_crates( 124 | dependencies: &[SourcedPackage], 125 | max_age: Duration, 126 | ) -> Result< 127 | ( 128 | BTreeMap>, 129 | BTreeMap>, 130 | ), 131 | io::Error, 132 | > { 133 | let crates_io_names = crate_names_from_source(dependencies, PkgSource::CratesIo); 134 | let mut client = RateLimitedClient::new(); 135 | let mut cached = CratesCache::new(); 136 | let using_cache = match cached.expire(max_age) { 137 | CacheState::Fresh => true, 138 | CacheState::Expired => { 139 | eprintln!( 140 | "\nIgnoring expired cache, older than {}.", 141 | // we use humantime rather than indicatif because we take humantime input 142 | // and here we simply repeat it back to the user 143 | humantime::format_duration(max_age) 144 | ); 145 | eprintln!(" Run `cargo supply-chain update` to update it."); 146 | false 147 | } 148 | CacheState::Unknown => { 149 | eprintln!("\nThe `crates.io` cache was not found or it is invalid."); 150 | eprintln!(" Run `cargo supply-chain update` to generate it."); 151 | false 152 | } 153 | }; 154 | let mut users: BTreeMap> = BTreeMap::new(); 155 | let mut teams: BTreeMap> = BTreeMap::new(); 156 | 157 | if using_cache { 158 | let age = cached.age().unwrap(); 159 | eprintln!( 160 | "\nUsing cached data. Cache age: {}", 161 | indicatif::HumanDuration(age) 162 | ); 163 | } else { 164 | eprintln!("\nFetching publisher info from crates.io"); 165 | eprintln!("This will take roughly 2 seconds per crate due to API rate limits"); 166 | } 167 | 168 | let bar = indicatif::ProgressBar::new(crates_io_names.len() as u64) 169 | .with_prefix("Preparing") 170 | .with_style( 171 | indicatif::ProgressStyle::default_bar() 172 | .template("{prefix:>12.bright.cyan} [{bar:27}] {pos:>4}/{len:4} ETA {eta:3} - {msg:.cyan}").unwrap() 173 | .progress_chars("=> ") 174 | ); 175 | 176 | for (i, crate_name) in crates_io_names.iter().enumerate() { 177 | bar.set_message(crate_name.clone()); 178 | bar.set_position((i + 1) as u64); 179 | let cached_users = cached.publisher_users(crate_name); 180 | let cached_teams = cached.publisher_teams(crate_name); 181 | if let (Some(pub_users), Some(pub_teams)) = (cached_users, cached_teams) { 182 | bar.set_prefix("Loading cache"); 183 | users.insert(crate_name.clone(), pub_users); 184 | teams.insert(crate_name.clone(), pub_teams); 185 | } else { 186 | // Handle crates not found in the cache by fetching live data for them 187 | bar.set_prefix("Downloading"); 188 | let pusers = publisher_users(&mut client, crate_name)?; 189 | users.insert(crate_name.clone(), pusers); 190 | let pteams = publisher_teams(&mut client, crate_name)?; 191 | teams.insert(crate_name.clone(), pteams); 192 | } 193 | } 194 | Ok((users, teams)) 195 | } 196 | -------------------------------------------------------------------------------- /src/subcommands/crates.rs: -------------------------------------------------------------------------------- 1 | use crate::publishers::{fetch_owners_of_crates, PublisherKind}; 2 | use crate::{ 3 | common::{comma_separated_list, complain_about_non_crates_io_crates, sourced_dependencies}, 4 | MetadataArgs, 5 | }; 6 | 7 | pub fn crates( 8 | metadata_args: MetadataArgs, 9 | diffable: bool, 10 | max_age: std::time::Duration, 11 | ) -> Result<(), anyhow::Error> { 12 | let dependencies = sourced_dependencies(metadata_args)?; 13 | complain_about_non_crates_io_crates(&dependencies); 14 | let (mut owners, publisher_teams) = fetch_owners_of_crates(&dependencies, max_age)?; 15 | 16 | for (crate_name, publishers) in publisher_teams { 17 | owners.entry(crate_name).or_default().extend(publishers); 18 | } 19 | 20 | let mut ordered_owners: Vec<_> = owners.into_iter().collect(); 21 | if diffable { 22 | // Sort alphabetically by crate name 23 | ordered_owners.sort_unstable_by_key(|(name, _)| name.clone()); 24 | } else { 25 | // Order by the number of owners, but put crates owned by teams first 26 | ordered_owners.sort_unstable_by_key(|(name, publishers)| { 27 | ( 28 | !publishers.iter().any(|p| p.kind == PublisherKind::team), // contains at least one team 29 | usize::MAX - publishers.len(), 30 | name.clone(), 31 | ) 32 | }); 33 | } 34 | for (_, publishers) in &mut ordered_owners { 35 | // For each crate put teams first 36 | publishers.sort_unstable_by_key(|p| (p.kind, p.login.clone())); 37 | } 38 | 39 | if !diffable { 40 | println!( 41 | "\nDependency crates with the people and teams that can publish them to crates.io:\n" 42 | ); 43 | } 44 | for (i, (crate_name, publishers)) in ordered_owners.iter().enumerate() { 45 | let pretty_publishers: Vec = publishers 46 | .iter() 47 | .map(|p| match p.kind { 48 | PublisherKind::team => format!("team \"{}\"", p.login), 49 | PublisherKind::user => p.login.to_string(), 50 | }) 51 | .collect(); 52 | let publishers_list = comma_separated_list(&pretty_publishers); 53 | if diffable { 54 | println!("{}: {}", crate_name, publishers_list); 55 | } else { 56 | println!("{}. {}: {}", i + 1, crate_name, publishers_list); 57 | } 58 | } 59 | 60 | if !ordered_owners.is_empty() { 61 | eprintln!("\nNote: there may be outstanding publisher invitations. crates.io provides no way to list them."); 62 | eprintln!("See https://github.com/rust-lang/crates.io/issues/2868 for more info."); 63 | } 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /src/subcommands/json.rs: -------------------------------------------------------------------------------- 1 | //! `json` subcommand is equivalent to `crates`, 2 | //! but provides structured output and more info about each publisher. 3 | use crate::publishers::{fetch_owners_of_crates, PublisherData}; 4 | use crate::{ 5 | common::{crate_names_from_source, sourced_dependencies, PkgSource}, 6 | MetadataArgs, 7 | }; 8 | use serde::Serialize; 9 | use std::collections::BTreeMap; 10 | 11 | #[cfg(test)] 12 | use schemars::JsonSchema; 13 | 14 | #[cfg_attr(test, derive(JsonSchema))] 15 | #[derive(Debug, Serialize, Default, Clone)] 16 | pub struct StructuredOutput { 17 | not_audited: NotAudited, 18 | /// Maps crate names to info about the publishers of each crate 19 | crates_io_crates: BTreeMap>, 20 | } 21 | 22 | #[cfg_attr(test, derive(JsonSchema))] 23 | #[derive(Debug, Serialize, Default, Clone)] 24 | pub struct NotAudited { 25 | /// Names of crates that are imported from a location in the local filesystem, not from a registry 26 | local_crates: Vec, 27 | /// Names of crates that are neither from crates.io nor from a local filesystem 28 | foreign_crates: Vec, 29 | } 30 | 31 | pub fn json( 32 | args: MetadataArgs, 33 | diffable: bool, 34 | max_age: std::time::Duration, 35 | ) -> Result<(), anyhow::Error> { 36 | let mut output = StructuredOutput::default(); 37 | let dependencies = sourced_dependencies(args)?; 38 | // Report non-crates.io dependencies 39 | output.not_audited.local_crates = crate_names_from_source(&dependencies, PkgSource::Local); 40 | output.not_audited.foreign_crates = crate_names_from_source(&dependencies, PkgSource::Foreign); 41 | output.not_audited.local_crates.sort_unstable(); 42 | output.not_audited.foreign_crates.sort_unstable(); 43 | // Fetch list of owners and publishers 44 | let (mut owners, publisher_teams) = fetch_owners_of_crates(&dependencies, max_age)?; 45 | // Merge the two maps we received into one 46 | for (crate_name, publishers) in publisher_teams { 47 | owners.entry(crate_name).or_default().extend(publishers); 48 | } 49 | // Sort the vectors of publisher data. This helps when diffing the output, 50 | // but we do it unconditionally because it's cheap and helps users pull less hair when debugging. 51 | for list in owners.values_mut() { 52 | list.sort_unstable_by_key(|x| x.id); 53 | } 54 | output.crates_io_crates = owners; 55 | // Print the result to stdout 56 | let stdout = std::io::stdout(); 57 | let handle = stdout.lock(); 58 | if diffable { 59 | serde_json::to_writer_pretty(handle, &output)?; 60 | } else { 61 | serde_json::to_writer(handle, &output)?; 62 | } 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /src/subcommands/json_schema.rs: -------------------------------------------------------------------------------- 1 | //! The schema for the JSON subcommand output 2 | 3 | use std::io::{Result, Write}; 4 | 5 | pub fn print_schema() -> Result<()> { 6 | writeln!(std::io::stdout(), "{}", JSON_SCHEMA)?; 7 | Ok(()) 8 | } 9 | 10 | const JSON_SCHEMA: &str = r##"{ 11 | "$schema": "http://json-schema.org/draft-07/schema#", 12 | "title": "StructuredOutput", 13 | "type": "object", 14 | "required": [ 15 | "crates_io_crates", 16 | "not_audited" 17 | ], 18 | "properties": { 19 | "crates_io_crates": { 20 | "description": "Maps crate names to info about the publishers of each crate", 21 | "type": "object", 22 | "additionalProperties": { 23 | "type": "array", 24 | "items": { 25 | "$ref": "#/definitions/PublisherData" 26 | } 27 | } 28 | }, 29 | "not_audited": { 30 | "$ref": "#/definitions/NotAudited" 31 | } 32 | }, 33 | "definitions": { 34 | "NotAudited": { 35 | "type": "object", 36 | "required": [ 37 | "foreign_crates", 38 | "local_crates" 39 | ], 40 | "properties": { 41 | "foreign_crates": { 42 | "description": "Names of crates that are neither from crates.io nor from a local filesystem", 43 | "type": "array", 44 | "items": { 45 | "type": "string" 46 | } 47 | }, 48 | "local_crates": { 49 | "description": "Names of crates that are imported from a location in the local filesystem, not from a registry", 50 | "type": "array", 51 | "items": { 52 | "type": "string" 53 | } 54 | } 55 | } 56 | }, 57 | "PublisherData": { 58 | "description": "Data about a single publisher received from a crates.io API endpoint", 59 | "type": "object", 60 | "required": [ 61 | "id", 62 | "kind", 63 | "login" 64 | ], 65 | "properties": { 66 | "avatar": { 67 | "description": "Avatar image URL", 68 | "type": [ 69 | "string", 70 | "null" 71 | ] 72 | }, 73 | "id": { 74 | "type": "integer", 75 | "format": "uint64", 76 | "minimum": 0.0 77 | }, 78 | "kind": { 79 | "$ref": "#/definitions/PublisherKind" 80 | }, 81 | "login": { 82 | "type": "string" 83 | }, 84 | "name": { 85 | "description": "Display name. It is NOT guaranteed to be unique!", 86 | "type": [ 87 | "string", 88 | "null" 89 | ] 90 | } 91 | } 92 | }, 93 | "PublisherKind": { 94 | "type": "string", 95 | "enum": [ 96 | "team", 97 | "user" 98 | ] 99 | } 100 | } 101 | }"##; 102 | 103 | #[cfg(test)] 104 | mod tests { 105 | use super::*; 106 | use crate::subcommands::json::StructuredOutput; 107 | use schemars::schema_for; 108 | 109 | #[test] 110 | fn test_json_schema() { 111 | let schema = schema_for!(StructuredOutput); 112 | let schema = serde_json::to_string_pretty(&schema).unwrap(); 113 | assert_eq!(schema, JSON_SCHEMA); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/subcommands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod crates; 2 | pub mod json; 3 | pub mod json_schema; 4 | pub mod publishers; 5 | pub mod update; 6 | 7 | pub use crates::crates; 8 | pub use json::json; 9 | pub use json_schema::print_schema; 10 | pub use publishers::publishers; 11 | pub use update::update; 12 | -------------------------------------------------------------------------------- /src/subcommands/publishers.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use crate::publishers::fetch_owners_of_crates; 4 | use crate::MetadataArgs; 5 | use crate::{ 6 | common::{comma_separated_list, complain_about_non_crates_io_crates, sourced_dependencies}, 7 | publishers::PublisherData, 8 | }; 9 | 10 | pub fn publishers( 11 | metadata_args: MetadataArgs, 12 | diffable: bool, 13 | max_age: std::time::Duration, 14 | ) -> Result<(), anyhow::Error> { 15 | let dependencies = sourced_dependencies(metadata_args)?; 16 | complain_about_non_crates_io_crates(&dependencies); 17 | let (publisher_users, publisher_teams) = fetch_owners_of_crates(&dependencies, max_age)?; 18 | 19 | // Group data by user rather than by crate 20 | let mut user_to_crate_map = transpose_publishers_map(&publisher_users); 21 | let mut team_to_crate_map = transpose_publishers_map(&publisher_teams); 22 | 23 | // Sort crate names alphabetically 24 | user_to_crate_map.values_mut().for_each(|c| c.sort()); 25 | team_to_crate_map.values_mut().for_each(|c| c.sort()); 26 | 27 | if diffable { 28 | // empty map just means 0 loop iterations here 29 | let sorted_map = sort_transposed_map_for_diffing(user_to_crate_map); 30 | for (user, crates) in &sorted_map { 31 | let crate_list = comma_separated_list(crates); 32 | println!("user \"{}\": {}", &user.login, crate_list); 33 | } 34 | } else if !publisher_users.is_empty() { 35 | println!("\nThe following individuals can publish updates for your dependencies:\n"); 36 | let map_for_display = sort_transposed_map_for_display(user_to_crate_map); 37 | for (i, (user, crates)) in map_for_display.iter().enumerate() { 38 | // We do not print usernames, since you can embed terminal control sequences in them 39 | // and erase yourself from the output that way. 40 | let crate_list = comma_separated_list(crates); 41 | println!(" {}. {} via crates: {}", i + 1, &user.login, crate_list); 42 | } 43 | eprintln!("\nNote: there may be outstanding publisher invitations. crates.io provides no way to list them."); 44 | eprintln!("See https://github.com/rust-lang/crates.io/issues/2868 for more info."); 45 | } 46 | 47 | if diffable { 48 | let sorted_map = sort_transposed_map_for_diffing(team_to_crate_map); 49 | for (team, crates) in &sorted_map { 50 | let crate_list = comma_separated_list(crates); 51 | println!("team \"{}\": {}", &team.login, crate_list); 52 | } 53 | } else if !publisher_teams.is_empty() { 54 | println!( 55 | "\nAll members of the following teams can publish updates for your dependencies:\n" 56 | ); 57 | let map_for_display = sort_transposed_map_for_display(team_to_crate_map); 58 | for (i, (team, crates)) in map_for_display.iter().enumerate() { 59 | let crate_list = comma_separated_list(crates); 60 | if let (true, Some(org)) = ( 61 | team.login.starts_with("github:"), 62 | team.login.split(':').nth(1), 63 | ) { 64 | println!( 65 | " {}. \"{}\" (https://github.com/{}) via crates: {}", 66 | i + 1, 67 | &team.login, 68 | org, 69 | crate_list 70 | ); 71 | } else { 72 | println!(" {}. \"{}\" via crates: {}", i + 1, &team.login, crate_list); 73 | } 74 | } 75 | eprintln!("\nGithub teams are black boxes. It's impossible to get the member list without explicit permission."); 76 | } 77 | Ok(()) 78 | } 79 | 80 | /// Turns a crate-to-publishers mapping into publisher-to-crates mapping. 81 | /// [`BTreeMap`] is used because [`PublisherData`] doesn't implement Hash. 82 | fn transpose_publishers_map( 83 | input: &BTreeMap>, 84 | ) -> BTreeMap> { 85 | let mut result: BTreeMap> = BTreeMap::new(); 86 | for (crate_name, publishers) in input.iter() { 87 | for publisher in publishers { 88 | result 89 | .entry(publisher.clone()) 90 | .or_default() 91 | .push(crate_name.clone()); 92 | } 93 | } 94 | result 95 | } 96 | 97 | /// Returns a Vec sorted so that publishers are sorted by the number of crates they control. 98 | /// If that number is the same, sort by login. 99 | fn sort_transposed_map_for_display( 100 | input: BTreeMap>, 101 | ) -> Vec<(PublisherData, Vec)> { 102 | let mut result: Vec<_> = input.into_iter().collect(); 103 | result.sort_unstable_by_key(|(publisher, crates)| { 104 | (usize::MAX - crates.len(), publisher.login.clone()) 105 | }); 106 | result 107 | } 108 | 109 | fn sort_transposed_map_for_diffing( 110 | input: BTreeMap>, 111 | ) -> Vec<(PublisherData, Vec)> { 112 | let mut result: Vec<_> = input.into_iter().collect(); 113 | result.sort_unstable_by_key(|(publisher, _crates)| publisher.login.clone()); 114 | result 115 | } 116 | -------------------------------------------------------------------------------- /src/subcommands/update.rs: -------------------------------------------------------------------------------- 1 | use crate::api_client::RateLimitedClient; 2 | use crate::crates_cache::{CratesCache, DownloadState}; 3 | use anyhow::bail; 4 | 5 | pub fn update(max_age: std::time::Duration) -> Result<(), anyhow::Error> { 6 | let mut cache = CratesCache::new(); 7 | let mut client = RateLimitedClient::new(); 8 | 9 | match cache.download(&mut client, max_age) { 10 | Ok(state) => match state { 11 | DownloadState::Fresh => eprintln!("No updates found"), 12 | DownloadState::Expired => { 13 | eprintln!("Successfully updated to the newest daily data dump."); 14 | } 15 | DownloadState::Stale => bail!("Latest daily data dump matches the previous version, which was considered outdated."), 16 | }, 17 | Err(error) => bail!("Could not update to the latest daily data dump!\n{}", error) 18 | } 19 | Ok(()) 20 | } 21 | --------------------------------------------------------------------------------