Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
98c2675
Redact credentials from every URL the CLI shows or logs
mikolalysenko Oct 7, 2026
b585b63
Keep git remote credentials out of the VEX product @id
mikolalysenko Oct 7, 2026
9e28f04
Drop Composer transport-options in the hosted lock rewrite
mikolalysenko Oct 7, 2026
ec7bde2
Merge origin/main into arch-fix/sec-credentials
mikolalysenko Oct 7, 2026
e075618
Tighten the URL redactor and share its host parser
mikolalysenko Oct 7, 2026
67e235b
Send every debug line through one redacting printer
mikolalysenko Oct 7, 2026
664811c
Redact hosted skip details with the shared redactor
mikolalysenko Oct 7, 2026
b8c130d
Pin redaction at the vendor warning and download error sites
mikolalysenko Oct 7, 2026
37351f2
Bound the composer dist edit to its entry
mikolalysenko Oct 7, 2026
61cb86b
Pin the redacted hosted skip details in the scan tests
mikolalysenko Oct 7, 2026
ee87b33
Merge origin/main into arch-fix/sec-credentials
claude Oct 7, 2026
24502da
Rename the secret loop variable in the redaction tests
mikolalysenko Oct 7, 2026
f21c383
Merge remote-tracking branch 'origin/main' into arch-fix/sec-credentials
claude Oct 7, 2026
fa788c6
Merge remote-tracking branch 'origin/main' into arch-fix/sec-credentials
mikolalysenko Oct 8, 2026
2dd091f
Merge origin/main into arch-fix/sec-credentials
mikolalysenko Oct 8, 2026
158b15f
Merge origin/main into arch-fix/sec-credentials
mikolalysenko Oct 8, 2026
7dbabfa
Warn that Composer rollback cannot restore transport-options
claude Oct 9, 2026
c1affdf
Scope the Composer restore warning to packagist options
claude Oct 9, 2026
c387851
Read the packagist host through url_hostname
claude Oct 9, 2026
85bb250
Merge origin/main (#1008) into arch-fix/sec-credentials
claude Oct 9, 2026
92bc8ba
Merge remote-tracking branch 'origin/main' into arch-fix/sec-credentials
claude Oct 9, 2026
1ff4262
Merge remote-tracking branch 'origin/main' into arch-fix/sec-credentials
claude Oct 9, 2026
14160e3
Merge origin/main into arch-fix/sec-credentials
claude Oct 9, 2026
af116f0
Merge branch 'main' into arch-fix/sec-credentials
mikolalysenko Oct 9, 2026
5f0bf77
Merge branch 'main' into arch-fix/sec-credentials
mikolalysenko Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions crates/socket-patch-cli/src/commands/vex_consumed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,15 +352,10 @@ fn registry_host(source: &str) -> Option<String> {
let url = source
.strip_prefix("sparse+")
.or_else(|| source.strip_prefix("registry+"))?;
let (_, rest) = url.split_once("://")?;
let authority = &rest[..rest.find(['/', '?', '#']).unwrap_or(rest.len())];
let hostport = authority.rsplit_once('@').map_or(authority, |(_, h)| h);
let host = if hostport.starts_with('[') {
&hostport[..=hostport.find(']')?]
} else {
hostport.split(':').next()?
};
(!host.is_empty()).then(|| host.to_ascii_lowercase())
if !url.contains("://") {
return None;
}
socket_patch_core::utils::redact::url_hostname(url).map(str::to_ascii_lowercase)
}

/// The `<host>-<hash>` name of a `…/registry/src/<host>-<hash>` source dir,
Expand Down
5 changes: 4 additions & 1 deletion crates/socket-patch-cli/src/update_notifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,10 @@ pub struct Notifier {

fn debug_log(debug: bool, message: &str) {
if debug {
eprintln!("[socket-patch update] {message}");
eprintln!(
"[socket-patch update] {}",
socket_patch_core::utils::redact::redact_urls_in(message)
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,10 +393,10 @@ async fn wheel_metadata_failures_fold_in_dep_order() {
for s in doc["redirect"]["skipped"].as_array().unwrap() {
if s["reason"] == "python_metadata_unavailable" {
let detail = s["detail"].as_str().unwrap();
assert!(
!detail.contains(&server.uri()),
"the hosted URL is redacted from the detail: {detail}"
);
// The detail quotes the URL through the shared redactor (this
// fixture's `/wheels/` URL carries no grant token; core's
// `hosted_skip_details_never_carry_the_grant_token` pins it).
assert!(!detail.is_empty(), "the detail names the failure: {doc:#}");
}
}
// The two good wheels still redirect; the refused two stay upstream.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,10 @@ async fn unfetchable_served_manifest_skips_the_patch() {
assert_eq!(skipped.len(), 1, "{doc:#}");
assert_eq!(skipped[0]["purl"], PURL, "{doc:#}");
let detail = skipped[0]["detail"].as_str().unwrap();
// The URL is quoted with its grant-token level redacted.
assert!(
!detail.contains(&server.uri()),
"the hosted URL is redacted: {detail}"
!detail.contains(TOKEN) && detail.contains(&format!("/<redacted>/{UUID}/")),
"the grant token is redacted: {detail}"
);
assert_eq!(doc["redirect"]["redirected"], 0, "{doc:#}");
assert_eq!(
Expand Down
74 changes: 63 additions & 11 deletions crates/socket-patch-core/src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ use crate::api::vendor_prefetch::VendorPrefetch;
pub use crate::api::vendor_prefetch::VendorPrefetchGuard;
use crate::constants::USER_AGENT as USER_AGENT_VALUE;
use crate::utils::digest::is_hex;
use crate::utils::env_compat::{is_debug_enabled, is_offline_env, proxy_url_from_env};
use crate::utils::env_compat::{is_offline_env, proxy_url_from_env};
use crate::utils::notice::{notice_once, Notice};
use crate::utils::redact::{redact_url, redact_urls_in};
use crate::utils::socket_cli_config;
use crate::utils::target::is_uuid_shaped;

Expand All @@ -38,10 +39,12 @@ static MULTI_ORG_SHOWN: AtomicBool = AtomicBool::new(false);
/// act on ("Connection refused", a DNS or TLS failure). Causes already
/// spelled out by an outer message are skipped.
fn network_error_detail(e: &reqwest::Error) -> String {
let mut msg = e.to_string();
// reqwest's text names the request URL: a grant URL, or one with
// userinfo from `--api-url` / a proxy, is quoted redacted.
let mut msg = redact_urls_in(&e.to_string()).into_owned();
let mut source = std::error::Error::source(e);
while let Some(cause) = source {
let part = cause.to_string();
let part = redact_urls_in(&cause.to_string()).into_owned();
if !part.is_empty() && !msg.contains(&part) {
msg.push_str(": ");
msg.push_str(&part);
Expand Down Expand Up @@ -95,9 +98,14 @@ fn status_error(head: &str, status: StatusCode, text: &str) -> String {
}
}

/// Log debug messages when debug mode is enabled.
/// Log debug messages when debug mode is enabled. Every URL in `message`
/// is redacted first: debug lines quote grant URLs and `--api-url`s, and
/// debug output is routinely pasted into CI logs and bug reports.
fn debug_log(message: &str) {
if is_debug_enabled() && !defer_debug_line(message) {
let Some(message) = crate::utils::env_compat::debug_message(message) else {
return;
};
if !defer_debug_line(&message) {
eprintln!("[socket-patch debug] {}", message);
}
}
Expand Down Expand Up @@ -1694,7 +1702,8 @@ impl ApiClient {
if !(url.starts_with("https://") || url.starts_with("http://")) {
return (
ServeDownload::Failed(ApiError::Other(format!(
"refusing non-http(s) artifact URL `{url}`"
"refusing non-http(s) artifact URL `{}`",
redact_url(url)
))),
None,
);
Expand Down Expand Up @@ -1843,8 +1852,14 @@ pub(crate) struct DeferredAttempt {
fn artifact_download_result(outcome: ServeDownload, url: &str) -> Result<Vec<u8>, ApiError> {
match outcome {
ServeDownload::Ok(bytes) => Ok(bytes),
ServeDownload::NotFound => Err(ApiError::Other(format!("artifact not found: {url}"))),
ServeDownload::Pending => Err(ApiError::Other(format!("artifact still building: {url}"))),
ServeDownload::NotFound => Err(ApiError::Other(format!(
"artifact not found: {}",
redact_url(url)
))),
ServeDownload::Pending => Err(ApiError::Other(format!(
"artifact still building: {}",
redact_url(url)
))),
ServeDownload::Failed(e) => Err(e),
}
}
Expand Down Expand Up @@ -2831,7 +2846,8 @@ impl ApiClient {
) -> Result<Vec<u8>, ApiError> {
if !(url.starts_with("https://") || url.starts_with("http://")) {
return Err(ApiError::Other(format!(
"refusing non-http(s) artifact URL `{url}`"
"refusing non-http(s) artifact URL `{}`",
redact_url(url)
)));
}
let attempts = self.vendor_retry.attempts.max(1);
Expand Down Expand Up @@ -2883,13 +2899,19 @@ impl ApiClient {
StatusCode::OK => {}
StatusCode::NOT_FOUND | StatusCode::GONE => {
return (
Err(ApiError::Other(format!("artifact not found: {url}"))),
Err(ApiError::Other(format!(
"artifact not found: {}",
redact_url(url)
))),
None,
)
}
StatusCode::REQUEST_TIMEOUT => {
return (
Err(ApiError::Other(format!("artifact still building: {url}"))),
Err(ApiError::Other(format!(
"artifact still building: {}",
redact_url(url)
))),
None,
)
}
Expand Down Expand Up @@ -5121,6 +5143,36 @@ mod vendor_package_tests {
}
}

/// The artifact errors quote the grant URL redacted: neither the grant
/// token nor userinfo reaches the error a caller shows.
#[tokio::test]
async fn download_artifact_errors_never_carry_a_credential() {
const GRANT: &str = "grant-level";
const UUID: &str = "7c8d9e0f-1a2b-4a1b-8c2d-3e4f5a6b7c8d";
let grant_path = format!("/patch/pypi/a/1.0.0/{GRANT}/{UUID}/a.whl");
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path(grant_path.as_str()))
.respond_with(ResponseTemplate::new(404))
.mount(&server)
.await;
let client = auth_client(server.uri());
for url in [
format!("ftp://u:pw@h.example{grant_path}"),
format!(
"{}{grant_path}",
server.uri().replace("http://", "http://u:pw@")
),
] {
let msg = match client.download_artifact(&url).await {
Err(ApiError::Other(msg)) => msg,
other => panic!("expected Other, got {other:?}"),
};
assert!(!msg.contains(GRANT) && !msg.contains("u:pw"), "{msg}");
assert!(msg.contains(&format!("/<redacted>/{UUID}/")), "{msg}");
}
}

/// A promised secondary that 404s must ERROR ("artifact not found"),
/// and a still-building 408 maps to "artifact still building" — the
/// arms encoding the no-soft-skip policy for promised artifacts.
Expand Down
4 changes: 1 addition & 3 deletions crates/socket-patch-core/src/crawlers/jvm_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,7 @@ pub fn push_classified(roots: &mut Vec<JvmCacheRoot>, root: JvmCacheRoot) -> boo

/// A `SOCKET_DEBUG` line from the JVM cache discovery.
pub(crate) fn debug_log(message: &str) {
if crate::utils::env_compat::is_debug_enabled() {
eprintln!("[socket-patch debug] {message}");
}
crate::utils::env_compat::debug_log("debug", message);
}

/// One installed-artifact cache to crawl.
Expand Down
5 changes: 3 additions & 2 deletions crates/socket-patch-core/src/formats/composer/hosted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,9 @@ pub(crate) fn rewrite_composer_lock(
};
let rewritten = (!already_redirected).then_some(rewritten);
// Drops the entry's `source` wherever it sits and the dist's
// `mirrors` (see `composer_source`); the edit spans both, so the
// ledger's fragment revert restores them byte-for-byte.
// `mirrors` (see `composer_source`); the edit spans both, so a
// ledger fragment revert restores them byte-for-byte (the upstream
// restore does not bring `transport-options` back; it warns).
let span = composer_source::DistSpan {
entry_start,
entry_end,
Expand Down
18 changes: 18 additions & 0 deletions crates/socket-patch-core/src/formats/composer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,24 @@ use crate::utils::digest::sha1_hex;
use crate::vendor::lock_inventory::{http_url, LockIntegrity, LockfileEntry, SourceKind};
use crate::vendor::path::{parse_vendor_path, VendorPathParts};

/// The lock-entry members that belong to the package's ORIGINAL repository
/// and must not survive a rewrite that points the entry somewhere else
/// (the hosted patch URL, a vendored path dist):
///
/// * `source`: the upstream VCS checkout Composer falls back to (see
/// [`source`]);
/// * `transport-options`: Composer copies the repository's `options`
/// (`http.header` auth tokens, `ssl` client certificates, `http.proxy`)
/// into every entry it resolves from it, and applies them to that entry's
/// dist download. Kept on a redirected entry, they send a private
/// registry's credentials to the new host (#399).
///
/// Both backends drop exactly these: the hosted splice
/// ([`source::apply_dist_edit`]) and the vendored rebuild
/// ([`crate::vendor::composer_lock`]'s `rewrite_lock_entry`, which then
/// writes its own `transport-options`).
pub(crate) const ORIGIN_BOUND_ENTRY_KEYS: [&str; 2] = ["source", "transport-options"];

// ── entry model ──

/// One entry of a parsed `composer.lock` (see [`composer_lock_packages`]).
Expand Down
Loading
Loading