upd-cli 0.1.4__tar.gz → 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. {upd_cli-0.1.4 → upd_cli-0.1.5}/CHANGELOG.md +7 -0
  2. {upd_cli-0.1.4 → upd_cli-0.1.5}/Cargo.lock +1 -1
  3. {upd_cli-0.1.4 → upd_cli-0.1.5}/Cargo.toml +1 -1
  4. {upd_cli-0.1.4 → upd_cli-0.1.5}/PKG-INFO +1 -1
  5. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/audit/mod.rs +208 -3
  6. {upd_cli-0.1.4 → upd_cli-0.1.5}/.mise.toml +0 -0
  7. {upd_cli-0.1.4 → upd_cli-0.1.5}/.pre-commit-config.yaml +0 -0
  8. {upd_cli-0.1.4 → upd_cli-0.1.5}/.pre-commit-hooks.yaml +0 -0
  9. {upd_cli-0.1.4 → upd_cli-0.1.5}/.rumdl.toml +0 -0
  10. {upd_cli-0.1.4 → upd_cli-0.1.5}/LICENSE +0 -0
  11. {upd_cli-0.1.4 → upd_cli-0.1.5}/Makefile +0 -0
  12. {upd_cli-0.1.4 → upd_cli-0.1.5}/README.md +0 -0
  13. {upd_cli-0.1.4 → upd_cli-0.1.5}/assets/logo-wide.svg +0 -0
  14. {upd_cli-0.1.4 → upd_cli-0.1.5}/assets/logo.svg +0 -0
  15. {upd_cli-0.1.4 → upd_cli-0.1.5}/pyproject.toml +0 -0
  16. {upd_cli-0.1.4 → upd_cli-0.1.5}/python/upd_cli/__init__.py +0 -0
  17. {upd_cli-0.1.4 → upd_cli-0.1.5}/python/upd_cli/__main__.py +0 -0
  18. {upd_cli-0.1.4 → upd_cli-0.1.5}/python/upd_cli/py.typed +0 -0
  19. {upd_cli-0.1.4 → upd_cli-0.1.5}/rust-toolchain.toml +0 -0
  20. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/align.rs +0 -0
  21. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/audit/cache.rs +0 -0
  22. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/audit/cvss.rs +0 -0
  23. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/cache.rs +0 -0
  24. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/cli.rs +0 -0
  25. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/config.rs +0 -0
  26. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/cooldown.rs +0 -0
  27. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/interactive.rs +0 -0
  28. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/lib.rs +0 -0
  29. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/lockfile.rs +0 -0
  30. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/main.rs +0 -0
  31. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/output.rs +0 -0
  32. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/registry/crates_io.rs +0 -0
  33. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/registry/github_releases.rs +0 -0
  34. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/registry/go_proxy.rs +0 -0
  35. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/registry/mock.rs +0 -0
  36. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/registry/mod.rs +0 -0
  37. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/registry/npm.rs +0 -0
  38. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/registry/nuget.rs +0 -0
  39. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/registry/pypi.rs +0 -0
  40. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/registry/rubygems.rs +0 -0
  41. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/registry/terraform.rs +0 -0
  42. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/registry/utils.rs +0 -0
  43. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/updater/cargo_toml.rs +0 -0
  44. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/updater/csproj.rs +0 -0
  45. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/updater/gemfile.rs +0 -0
  46. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/updater/github_actions.rs +0 -0
  47. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/updater/go_mod.rs +0 -0
  48. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/updater/mise.rs +0 -0
  49. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/updater/mod.rs +0 -0
  50. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/updater/npm_range.rs +0 -0
  51. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/updater/package_json.rs +0 -0
  52. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/updater/pre_commit.rs +0 -0
  53. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/updater/pyproject.rs +0 -0
  54. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/updater/requirements.rs +0 -0
  55. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/updater/terraform.rs +0 -0
  56. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/version/compare.rs +0 -0
  57. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/version/mod.rs +0 -0
  58. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/version/pep440.rs +0 -0
  59. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/version/semver_util.rs +0 -0
  60. {upd_cli-0.1.4 → upd_cli-0.1.5}/src/version/tag.rs +0 -0
  61. {upd_cli-0.1.4 → upd_cli-0.1.5}/tests/audit_offline.rs +0 -0
  62. {upd_cli-0.1.4 → upd_cli-0.1.5}/tests/audit_sarif.rs +0 -0
  63. {upd_cli-0.1.4 → upd_cli-0.1.5}/tests/audit_severity.rs +0 -0
  64. {upd_cli-0.1.4 → upd_cli-0.1.5}/tests/bump_filter.rs +0 -0
  65. {upd_cli-0.1.4 → upd_cli-0.1.5}/tests/cooldown_e2e.rs +0 -0
  66. {upd_cli-0.1.4 → upd_cli-0.1.5}/tests/discovery_no_ignore.rs +0 -0
  67. {upd_cli-0.1.4 → upd_cli-0.1.5}/tests/exit_codes.rs +0 -0
  68. {upd_cli-0.1.4 → upd_cli-0.1.5}/tests/fix_audit.rs +0 -0
  69. {upd_cli-0.1.4 → upd_cli-0.1.5}/tests/format_json.rs +0 -0
  70. {upd_cli-0.1.4 → upd_cli-0.1.5}/tests/help_text.rs +0 -0
  71. {upd_cli-0.1.4 → upd_cli-0.1.5}/tests/interactive_tty.rs +0 -0
  72. {upd_cli-0.1.4 → upd_cli-0.1.5}/tests/invalid_positional.rs +0 -0
  73. {upd_cli-0.1.4 → upd_cli-0.1.5}/tests/no_args_scope.rs +0 -0
  74. {upd_cli-0.1.4 → upd_cli-0.1.5}/tests/output_streams.rs +0 -0
  75. {upd_cli-0.1.4 → upd_cli-0.1.5}/tests/package_filter.rs +0 -0
  76. {upd_cli-0.1.4 → upd_cli-0.1.5}/vership.toml +0 -0
@@ -14,6 +14,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
14
 
15
15
 
16
16
 
17
+
18
+ ## [0.1.5](https://github.com/rvben/upd/compare/v0.1.4...v0.1.5) - 2026-04-28
19
+
20
+ ### Added
21
+
22
+ - **audit**: include package names and HTTP body in OSV error diagnostics ([87b9aa8](https://github.com/rvben/upd/commit/87b9aa8d27545abf449461c3c3d096a661fa377f))
23
+
17
24
  ## [0.1.4](https://github.com/rvben/upd/compare/v0.1.3...v0.1.4) - 2026-04-28
18
25
 
19
26
  ### Added
@@ -2024,7 +2024,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
2024
2024
 
2025
2025
  [[package]]
2026
2026
  name = "upd"
2027
- version = "0.1.4"
2027
+ version = "0.1.5"
2028
2028
  dependencies = [
2029
2029
  "anyhow",
2030
2030
  "async-trait",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "upd"
3
- version = "0.1.4"
3
+ version = "0.1.5"
4
4
  edition = "2024"
5
5
  rust-version = "1.95.0"
6
6
  description = "A fast dependency updater for Python, Node.js, Rust, Go, Ruby, Terraform, GitHub Actions, pre-commit, and Mise projects"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: upd-cli
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -20,6 +20,35 @@ const BATCH_SIZE: usize = 1000;
20
20
  /// Maximum concurrent requests for fetching vulnerability details
21
21
  const MAX_CONCURRENT_REQUESTS: usize = 20;
22
22
 
23
+ /// Cap for how many bytes of a non-success OSV response body we surface in
24
+ /// an error message. A proxy or CDN can return arbitrarily large HTML error
25
+ /// pages on the failure path, so we bound the read at the network layer
26
+ /// (via `Response::chunk`) rather than buffering the whole body and then
27
+ /// truncating. 1 KiB comfortably fits typical OSV/proxy diagnostics.
28
+ const ERROR_BODY_LIMIT: usize = 1024;
29
+
30
+ /// Read up to `max` bytes from `response`'s body and return them as a
31
+ /// lossy UTF-8 string. Stops early at the first chunk that fills the cap
32
+ /// so an oversized or slow body cannot stall the call up to the client
33
+ /// timeout or balloon memory beyond `max` bytes.
34
+ async fn read_bounded_body(response: &mut reqwest::Response, max: usize) -> String {
35
+ let mut buf: Vec<u8> = Vec::with_capacity(max);
36
+ while buf.len() < max {
37
+ match response.chunk().await {
38
+ Ok(Some(chunk)) => {
39
+ let remaining = max - buf.len();
40
+ let take = chunk.len().min(remaining);
41
+ buf.extend_from_slice(&chunk[..take]);
42
+ if take < chunk.len() {
43
+ break;
44
+ }
45
+ }
46
+ Ok(None) | Err(_) => break,
47
+ }
48
+ }
49
+ String::from_utf8_lossy(&buf).into_owned()
50
+ }
51
+
23
52
  /// Ecosystem names for OSV API
24
53
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
25
54
  pub enum Ecosystem {
@@ -273,7 +302,12 @@ impl OsvClient {
273
302
  }
274
303
  }
275
304
  Err(e) => {
276
- result.errors.push(format!("Batch query failed: {}", e));
305
+ let pkg_names: Vec<&str> = chunk.iter().map(|p| p.name.as_str()).collect();
306
+ result.errors.push(format!(
307
+ "Batch query failed for [{}]: {}",
308
+ pkg_names.join(", "),
309
+ e
310
+ ));
277
311
  }
278
312
  }
279
313
  }
@@ -299,7 +333,7 @@ impl OsvClient {
299
333
 
300
334
  let request = OsvBatchRequest { queries };
301
335
 
302
- let response = self
336
+ let mut response = self
303
337
  .client
304
338
  .post(format!("{}/querybatch", self.base_url))
305
339
  .json(&request)
@@ -307,7 +341,13 @@ impl OsvClient {
307
341
  .await?;
308
342
 
309
343
  if !response.status().is_success() {
310
- anyhow::bail!("OSV API error: HTTP {}", response.status());
344
+ let status = response.status();
345
+ let snippet = read_bounded_body(&mut response, ERROR_BODY_LIMIT).await;
346
+ if snippet.is_empty() {
347
+ anyhow::bail!("OSV API error: HTTP {}", status);
348
+ } else {
349
+ anyhow::bail!("OSV API error: HTTP {}: {}", status, snippet);
350
+ }
311
351
  }
312
352
 
313
353
  let batch_response: OsvBatchResponse = response.json().await?;
@@ -1003,4 +1043,169 @@ mod tests {
1003
1043
  assert_eq!(result.safe_count, 1);
1004
1044
  // wiremock will assert that no requests were received when the server drops.
1005
1045
  }
1046
+
1047
+ // ─── batch failure error message ─────────────────────────────────────────
1048
+
1049
+ #[tokio::test]
1050
+ async fn batch_failure_error_includes_package_names_status_and_body() {
1051
+ use wiremock::matchers::{method, path};
1052
+ use wiremock::{Mock, MockServer, ResponseTemplate};
1053
+
1054
+ let server = MockServer::start().await;
1055
+
1056
+ // OSV returns a non-success response with a body. Both should surface
1057
+ // in the AuditResult error so operators can diagnose the failure.
1058
+ Mock::given(method("POST"))
1059
+ .and(path("/querybatch"))
1060
+ .respond_with(
1061
+ ResponseTemplate::new(503).set_body_string("upstream temporarily unavailable"),
1062
+ )
1063
+ .mount(&server)
1064
+ .await;
1065
+
1066
+ let client = OsvClient::with_base_url(server.uri());
1067
+ let pkgs = vec![
1068
+ Package {
1069
+ name: "requests".into(),
1070
+ version: "2.0.0".into(),
1071
+ ecosystem: Ecosystem::PyPI,
1072
+ },
1073
+ Package {
1074
+ name: "boto3".into(),
1075
+ version: "1.0.0".into(),
1076
+ ecosystem: Ecosystem::PyPI,
1077
+ },
1078
+ ];
1079
+
1080
+ let result = client.check_packages(&pkgs).await.unwrap();
1081
+
1082
+ assert!(result.vulnerable.is_empty());
1083
+ assert_eq!(
1084
+ result.errors.len(),
1085
+ 1,
1086
+ "one error for the failing batch, got {:?}",
1087
+ result.errors
1088
+ );
1089
+ let err = &result.errors[0];
1090
+ assert!(
1091
+ err.contains("requests") && err.contains("boto3"),
1092
+ "error should list the failing batch's package names: {err}"
1093
+ );
1094
+ assert!(
1095
+ err.contains("503"),
1096
+ "error should include the HTTP status: {err}"
1097
+ );
1098
+ assert!(
1099
+ err.contains("upstream temporarily unavailable"),
1100
+ "error should include the response body: {err}"
1101
+ );
1102
+ }
1103
+
1104
+ #[tokio::test]
1105
+ async fn batch_failure_stops_reading_at_cap_when_body_stalls() {
1106
+ // Stand up a TCP server that lies about Content-Length: it claims
1107
+ // 1 MB but only delivers 2 KiB, then stalls forever. The bounded
1108
+ // read in query_batch must fill its 1 KiB cap from the head of
1109
+ // the body and return immediately. An unbounded read (e.g.
1110
+ // `response.text().await`) would block until either the rest of
1111
+ // the body arrives or the 30 s client timeout fires.
1112
+ use std::time::{Duration, Instant};
1113
+ use tokio::io::{AsyncReadExt, AsyncWriteExt};
1114
+ use tokio::net::TcpListener;
1115
+
1116
+ let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1117
+ let addr = listener.local_addr().unwrap();
1118
+
1119
+ let server = tokio::spawn(async move {
1120
+ let (mut stream, _) = listener.accept().await.unwrap();
1121
+
1122
+ // Drain the request until we see the empty header line.
1123
+ let mut buf = [0u8; 4096];
1124
+ loop {
1125
+ let n = match stream.read(&mut buf).await {
1126
+ Ok(n) if n > 0 => n,
1127
+ _ => return,
1128
+ };
1129
+ if buf[..n].windows(4).any(|w| w == b"\r\n\r\n") {
1130
+ break;
1131
+ }
1132
+ }
1133
+
1134
+ // Headers promise 1 MB; we send only 2 KiB then stall.
1135
+ let head = b"HTTP/1.1 503 Service Unavailable\r\n\
1136
+ Content-Type: text/plain\r\n\
1137
+ Content-Length: 1000000\r\n\
1138
+ Connection: close\r\n\r\n";
1139
+ let _ = stream.write_all(head).await;
1140
+ let _ = stream.write_all(&vec![b'x'; 2048]).await;
1141
+ let _ = stream.flush().await;
1142
+
1143
+ // Park the task so the connection stays open without further
1144
+ // bytes. The test will abort the handle after the client
1145
+ // returns.
1146
+ std::future::pending::<()>().await;
1147
+ });
1148
+
1149
+ let client = OsvClient::with_base_url(format!("http://{}", addr));
1150
+ let started = Instant::now();
1151
+ let outcome = tokio::time::timeout(
1152
+ Duration::from_secs(10),
1153
+ client.check_packages(&[Package {
1154
+ name: "pkg".into(),
1155
+ version: "1.0.0".into(),
1156
+ ecosystem: Ecosystem::PyPI,
1157
+ }]),
1158
+ )
1159
+ .await;
1160
+ let elapsed = started.elapsed();
1161
+
1162
+ server.abort();
1163
+
1164
+ let result = outcome
1165
+ .expect(
1166
+ "OSV call must return promptly when the body stalls; \
1167
+ if it didn't, the body is being read unbounded",
1168
+ )
1169
+ .unwrap();
1170
+
1171
+ assert_eq!(result.errors.len(), 1, "{:?}", result.errors);
1172
+ let err = &result.errors[0];
1173
+ assert!(err.contains("503"), "should include status: {err}");
1174
+ // 5 s leaves a >100x margin over the expected ~1 RTT return
1175
+ // time, while staying well under the 30 s client timeout that
1176
+ // the buggy implementation would hit.
1177
+ assert!(
1178
+ elapsed < Duration::from_secs(5),
1179
+ "bounded read should return promptly; took {:?}",
1180
+ elapsed
1181
+ );
1182
+ }
1183
+
1184
+ #[tokio::test]
1185
+ async fn batch_failure_error_includes_package_names_on_network_error() {
1186
+ // Point at a closed port so reqwest fails to connect. Package names
1187
+ // should still be embedded in the resulting error message.
1188
+ let client = OsvClient::with_base_url("http://127.0.0.1:1/".to_string());
1189
+ let pkgs = vec![
1190
+ Package {
1191
+ name: "lonely-pkg".into(),
1192
+ version: "1.0.0".into(),
1193
+ ecosystem: Ecosystem::PyPI,
1194
+ },
1195
+ Package {
1196
+ name: "another-pkg".into(),
1197
+ version: "2.0.0".into(),
1198
+ ecosystem: Ecosystem::PyPI,
1199
+ },
1200
+ ];
1201
+
1202
+ let result = client.check_packages(&pkgs).await.unwrap();
1203
+
1204
+ assert_eq!(result.errors.len(), 1, "{:?}", result.errors);
1205
+ let err = &result.errors[0];
1206
+ assert!(
1207
+ err.contains("lonely-pkg") && err.contains("another-pkg"),
1208
+ "network-level errors should still name the failing packages: {err}"
1209
+ );
1210
+ }
1006
1211
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes