Skip to content
Open
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: 4 additions & 0 deletions crates/atuin-client/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@
## 5. Stripe live/test keys
# secrets_filter = true

## Defaults to false. If secrets_filter is also enabled, then when a secret is detected, secrets will instead be redacted. The secret
## itself will be replaced with the string "[REDACTED]" instead of filtering out the entire command.
# secrets_redact = true

## Defaults to true. If enabled, upon hitting enter Atuin will immediately execute the command. Press tab to return to the shell and edit.
# This applies for new installs. Old installs will keep the old behaviour unless configured otherwise.
enter_accept = true
Expand Down
98 changes: 95 additions & 3 deletions crates/atuin-client/src/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use atuin_common::utils::uuid_v7;

use eyre::{Result, bail, eyre};

use crate::secrets::SECRET_PATTERNS_RE;
use crate::secrets::{SECRET_PATTERNS_RE, redact_secrets};
use crate::settings::Settings;
use crate::utils::get_host_user;
use time::OffsetDateTime;
Expand Down Expand Up @@ -374,11 +374,40 @@ impl History {
}

pub fn should_save(&self, settings: &Settings) -> bool {
!(self.command.starts_with(' ')
if self.command.starts_with(' ')
|| self.command.is_empty()
|| settings.history_filter.is_match(&self.command)
|| settings.cwd_filter.is_match(&self.cwd)
|| (settings.secrets_filter && SECRET_PATTERNS_RE.is_match(&self.command)))
{
return false;
}

if settings.secrets_filter && !settings.secrets_redact {
!SECRET_PATTERNS_RE.is_match(&self.command)
} else {
debug_assert!(
!settings.secrets_filter || settings.secrets_redact,
"Only return true if secrets_filter is off or redactions are enabled!"
);
// secrets_redact is enabled, so `redact_if_needed` will remove the secret from the
// command, therefore it is save to save.
true
Comment on lines +388 to +394
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This true looks scary so I added a debug_assert! to make sure we're really sure we can safely pass true 😅

}
}

/// Redacts secrets from the command if needed based on settings.
/// Returns a new History with the redacted command, or self if no redaction needed.
pub fn redact_if_needed(&self, settings: &Settings) -> Self {
if settings.secrets_filter
&& settings.secrets_redact
&& SECRET_PATTERNS_RE.is_match(&self.command)
{
let mut redacted = self.clone();
redacted.command = redact_secrets(&self.command);
redacted
} else {
self.clone()
}
}
}

Expand All @@ -397,6 +426,8 @@ mod tests {
let settings = Settings {
cwd_filter: RegexSet::new(["^/supasecret"]).unwrap(),
history_filter: RegexSet::new(["^psql"]).unwrap(),
secrets_filter: true,
secrets_redact: false,
..Settings::utc()
};

Expand Down Expand Up @@ -467,6 +498,67 @@ mod tests {
assert!(stripe_key.should_save(&settings));
}

#[test]
fn redact_secrets() {
let settings = Settings {
secrets_filter: true,
secrets_redact: true,
..Settings::utc()
};

let stripe_key: History = History::capture()
.timestamp(time::OffsetDateTime::now_utc())
.command("curl foo.com/bar?key=sk_test_1234567890abcdefghijklmn")
.cwd("/")
.build()
.into();

assert!(stripe_key.should_save(&settings));

let redacted = stripe_key.redact_if_needed(&settings);
assert_eq!(redacted.command, "curl foo.com/bar?key=[REDACTED]");
}

#[test]
fn filter_secrets() {
let settings = Settings {
secrets_filter: true,
secrets_redact: false,
..Settings::utc()
};

let stripe_key: History = History::capture()
.timestamp(time::OffsetDateTime::now_utc())
.command("curl foo.com/bar?key=sk_test_1234567890abcdefghijklmn")
.cwd("/")
.build()
.into();

assert!(!stripe_key.should_save(&settings));
}

#[test]
fn redact_multiple_secrets() {
let settings = Settings {
secrets_filter: true,
secrets_redact: true,
..Settings::utc()
};

let multi_secret: History = History::capture()
.timestamp(time::OffsetDateTime::now_utc())
.command("export AWS_SECRET_ACCESS_KEY=foo && curl -H 'Authorization: Bearer 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' api.github.com")
.cwd("/")
.build()
.into();

let redacted = multi_secret.redact_if_needed(&settings);
assert_eq!(
redacted.command,
"export AWS_SECRET_ACCESS_KEY=foo && curl -H 'Authorization: Bearer [REDACTED]' api.github.com"
);
}

#[test]
fn test_serialize_deserialize() {
let bytes = [
Expand Down
20 changes: 19 additions & 1 deletion crates/atuin-client/src/secrets.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// This file will probably trigger a lot of scanners. Sorry.

use regex::RegexSet;
use regex::{Regex, RegexSet};
use std::sync::LazyLock;

pub enum TestValue<'a> {
Expand Down Expand Up @@ -153,6 +153,24 @@ pub static SECRET_PATTERNS_RE: LazyLock<RegexSet> = LazyLock::new(|| {
RegexSet::new(exprs).expect("Failed to build secrets regex")
});

static SECRET_PATTERNS_INDIVIDUAL: LazyLock<Vec<Regex>> = LazyLock::new(|| {
SECRET_PATTERNS
.iter()
.map(|f| Regex::new(f.1).expect("Failed to compile secret pattern"))
.collect()
});

pub fn redact_secrets(command: &str) -> String {
let mut result = command.to_string();
let matches = SECRET_PATTERNS_RE.matches(command);
for pattern_idx in matches.iter() {
if let Some(regex) = SECRET_PATTERNS_INDIVIDUAL.get(pattern_idx) {
result = regex.replace_all(&result, "[REDACTED]").to_string();
}
}
result
}

#[cfg(test)]
mod tests {
use regex::Regex;
Expand Down
2 changes: 2 additions & 0 deletions crates/atuin-client/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ pub struct Settings {
pub cwd_filter: RegexSet,

pub secrets_filter: bool,
pub secrets_redact: bool,
pub workspaces: bool,
pub ctrl_n_shortcuts: bool,

Expand Down Expand Up @@ -793,6 +794,7 @@ impl Settings {
.set_default("workspaces", false)?
.set_default("ctrl_n_shortcuts", false)?
.set_default("secrets_filter", true)?
.set_default("secrets_redact", false)?
.set_default("network_connect_timeout", 5)?
.set_default("network_timeout", 30)?
.set_default("local_timeout", 2.0)?
Expand Down
4 changes: 4 additions & 0 deletions crates/atuin/src/command/client/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,8 @@ impl Cmd {
return Ok(());
}

let h = h.redact_if_needed(settings);

// print the ID
// we use this as the key for calling end
println!("{}", h.id);
Expand Down Expand Up @@ -393,6 +395,8 @@ impl Cmd {
return Ok(());
}

let h = h.redact_if_needed(settings);

let resp = atuin_daemon::client::HistoryClient::new(
#[cfg(not(unix))]
settings.daemon.tcp_port,
Expand Down