Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 45 additions & 1 deletion crates/trace-share-core/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use anyhow::{Context, Result};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::{env, fs, path::PathBuf};
use std::{
env, fs,
path::{Path, PathBuf},
};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpstashConfig {
Expand Down Expand Up @@ -174,3 +177,44 @@ pub fn ensure_dirs() -> Result<()> {
fs::create_dir_all(&data_dir)?;
Ok(())
}

pub fn validate_network_url(url: &str, label: &str) -> Result<()> {
let parsed = reqwest::Url::parse(url).with_context(|| format!("invalid {label} URL: {url}"))?;

if parsed.scheme() == "https" {
return Ok(());
}

if parsed.scheme() == "http" && (allow_insecure_http() || is_loopback_http(&parsed)) {
return Ok(());
}

anyhow::bail!(
"insecure {label} URL scheme '{}' is not allowed (use https; http is only allowed for loopback or when TRACE_SHARE_ALLOW_INSECURE_HTTP=1)",
parsed.scheme()
);
}

fn allow_insecure_http() -> bool {
env::var("TRACE_SHARE_ALLOW_INSECURE_HTTP")
.ok()
.map(|v| matches!(v.as_str(), "1" | "true" | "TRUE" | "yes"))
.unwrap_or(false)
}

fn is_loopback_http(url: &reqwest::Url) -> bool {
matches!(
url.host_str(),
Some("localhost") | Some("127.0.0.1") | Some("::1")
)
}

pub fn write_private_file(path: &Path, contents: &[u8]) -> Result<()> {
fs::write(path, contents)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
}
Ok(())
}
10 changes: 6 additions & 4 deletions crates/trace-share-core/src/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::fs;
use tokio::time::{Duration, sleep};

use crate::{
config::{AppConfig, data_dir},
config::{AppConfig, data_dir, validate_network_url, write_private_file},
episode::EpisodeRecord,
models::ChunkDocument,
sanitize::contains_sensitive_patterns,
Expand Down Expand Up @@ -42,14 +42,15 @@ pub async fn publish_upsert_data(config: &AppConfig, docs: &[ChunkDocument]) ->
.rest_url
.as_ref()
.context("missing UPSTASH_VECTOR_REST_URL")?;
validate_network_url(rest_url, "Upstash REST")?;
let token = config
.upstash
.rest_token
.as_ref()
.context("missing UPSTASH_VECTOR_REST_TOKEN")?;

let endpoint = format!("{}/upsert-data", rest_url.trim_end_matches('/'));
let client = reqwest::Client::new();
let client = reqwest::Client::builder().no_proxy().build()?;

let payload = json!({
"vectors": anonymized_docs.iter().map(|doc| {
Expand Down Expand Up @@ -133,7 +134,7 @@ pub fn load_or_create_anonymization_salt() -> Result<String> {
}
}
let salt = uuid::Uuid::new_v4().to_string();
fs::write(path, &salt)?;
write_private_file(&path, salt.as_bytes())?;
Ok(salt)
}

Expand All @@ -148,6 +149,7 @@ pub async fn index_episode_pointer(
.rest_url
.as_ref()
.context("missing UPSTASH_VECTOR_REST_URL")?;
validate_network_url(rest_url, "Upstash REST")?;
let token = config
.upstash
.rest_token
Expand All @@ -156,7 +158,7 @@ pub async fn index_episode_pointer(

let salt = load_or_create_anonymization_salt()?;
let endpoint = format!("{}/upsert-data", rest_url.trim_end_matches('/'));
let client = reqwest::Client::new();
let client = reqwest::Client::builder().no_proxy().build()?;

let metadata = serde_json::json!({
"kind": "episode_pointer",
Expand Down
45 changes: 45 additions & 0 deletions crates/trace-share-core/src/sanitize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,19 @@ fn extract_event_index(path_text: &str) -> Option<usize> {
}

fn find_gitleaks_binary() -> Option<PathBuf> {
if let Some(configured) = std::env::var_os("TRACE_SHARE_GITLEAKS_PATH") {
let configured_path = PathBuf::from(configured);
if configured_path.is_absolute() && configured_path.exists() {
return Some(configured_path);
}
return None;
}

let path = std::env::var_os("PATH")?;
std::env::split_paths(&path).find_map(|dir| {
if !is_trusted_gitleaks_dir(&dir) {
return None;
}
let candidate = dir.join("gitleaks");
if candidate.exists() {
return Some(candidate);
Expand All @@ -365,6 +376,40 @@ fn find_gitleaks_binary() -> Option<PathBuf> {
})
}

fn is_trusted_gitleaks_dir(dir: &std::path::Path) -> bool {
if let Ok(home) = std::env::var("HOME") {
let home_path = PathBuf::from(home);
if dir.starts_with(home_path) {
return false;
}
}

#[cfg(unix)]
{
return dir.starts_with("/usr/bin")
|| dir.starts_with("/usr/local/bin")
|| dir.starts_with("/opt/homebrew/bin")
|| dir.starts_with("/bin")
|| dir.starts_with("/sbin")
|| dir.starts_with("/usr/sbin");
}

#[cfg(windows)]
{
if let Ok(program_files) = std::env::var("ProgramFiles") {
if dir.starts_with(PathBuf::from(program_files)) {
return true;
}
}
if let Ok(program_files_x86) = std::env::var("ProgramFiles(x86)") {
if dir.starts_with(PathBuf::from(program_files_x86)) {
return true;
}
}
return false;
}
}

#[derive(Debug, Clone, Default, Deserialize)]
struct GitleaksFinding {
#[serde(rename = "File")]
Expand Down
7 changes: 5 additions & 2 deletions crates/trace-share-core/src/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::{
use tokio::time::{Duration, sleep};

use crate::{
config::AppConfig,
config::{AppConfig, validate_network_url},
episode::{EpisodeRecord, derive_sft, derive_tooltrace},
};

Expand Down Expand Up @@ -349,11 +349,13 @@ async fn publish_snapshot_to_worker(
.base_url
.as_ref()
.context("missing TRACE_SHARE_WORKER_BASE_URL")?;
validate_network_url(base_url, "worker base")?;
let endpoint = format!("{}/v1/snapshots", base_url.trim_end_matches('/'));
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(
config.worker.timeout_seconds.max(5),
))
.no_proxy()
.build()?;

let payload = serde_json::json!({
Expand Down Expand Up @@ -414,13 +416,14 @@ async fn index_snapshot_pointer(
.rest_url
.as_ref()
.context("missing UPSTASH_VECTOR_REST_URL")?;
validate_network_url(rest_url, "Upstash REST")?;
let token = config
.upstash
.rest_token
.as_ref()
.context("missing UPSTASH_VECTOR_REST_TOKEN")?;
let endpoint = format!("{}/upsert-data", rest_url.trim_end_matches('/'));
let client = reqwest::Client::new();
let client = reqwest::Client::builder().no_proxy().build()?;

let payload = serde_json::json!({
"vectors": [
Expand Down
10 changes: 7 additions & 3 deletions crates/trace-share-core/src/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ use std::{
};
use tracing::warn;

use crate::config::{AppConfig, data_dir, default_sources_path};
use crate::config::{
AppConfig, data_dir, default_sources_path, validate_network_url, write_private_file,
};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceDef {
Expand Down Expand Up @@ -174,7 +176,7 @@ pub fn add_local_source(config: &AppConfig, source: SourceDef) -> Result<PathBuf

manifest.sources.sort_by(|a, b| a.id.cmp(&b.id));
let text = toml::to_string_pretty(&manifest)?;
fs::write(&path, text)?;
write_private_file(&path, text.as_bytes())?;
Ok(path)
}

Expand All @@ -184,6 +186,7 @@ pub async fn load_remote_registry(config: &AppConfig) -> Result<SourceManifest>
.url
.clone()
.context("remote registry url missing")?;
validate_network_url(&url, "remote registry")?;

let cache_path = data_dir()?.join("registry-cache.json");
let cached = read_cache(&cache_path).ok();
Expand Down Expand Up @@ -233,7 +236,8 @@ pub async fn load_remote_registry(config: &AppConfig) -> Result<SourceManifest>
etag,
manifest: manifest.clone(),
};
fs::write(cache_path, serde_json::to_vec_pretty(&snapshot)?)?;
let snapshot_bytes = serde_json::to_vec_pretty(&snapshot)?;
write_private_file(&cache_path, &snapshot_bytes)?;
Ok(manifest)
}

Expand Down
11 changes: 10 additions & 1 deletion crates/trace-share-core/src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use tokio::time::{Duration, sleep};

use crate::{config::AppConfig, episode::EpisodeRecord};
use crate::{
config::{AppConfig, validate_network_url},
episode::EpisodeRecord,
};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerUploadResponse {
Expand Down Expand Up @@ -51,12 +54,14 @@ async fn upload_episode_legacy(
.base_url
.as_ref()
.context("missing TRACE_SHARE_WORKER_BASE_URL")?;
validate_network_url(base_url, "worker base")?;

let endpoint = format!("{}/v1/episodes", base_url.trim_end_matches('/'));
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(
config.worker.timeout_seconds.max(5),
))
.no_proxy()
.build()?;

let mut attempt: u32 = 0;
Expand Down Expand Up @@ -103,12 +108,14 @@ async fn upload_episode_presigned(
.base_url
.as_ref()
.context("missing TRACE_SHARE_WORKER_BASE_URL")?;
validate_network_url(base_url, "worker base")?;
let presign_endpoint = format!("{}/v1/episodes/presign", base_url.trim_end_matches('/'));
let complete_endpoint = format!("{}/v1/episodes/complete", base_url.trim_end_matches('/'));
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(
config.worker.timeout_seconds.max(5),
))
.no_proxy()
.build()?;

let presign_payload = serde_json::json!({
Expand Down Expand Up @@ -163,12 +170,14 @@ pub async fn push_revocation(
.base_url
.as_ref()
.context("missing TRACE_SHARE_WORKER_BASE_URL")?;
validate_network_url(base_url, "worker base")?;

let endpoint = format!("{}/v1/revocations", base_url.trim_end_matches('/'));
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(
config.worker.timeout_seconds.max(5),
))
.no_proxy()
.build()?;

let payload = serde_json::json!({
Expand Down
Loading
Loading