Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -137,6 +137,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 @@ -494,6 +494,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 @@ -791,6 +792,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
Loading