1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#![doc(
    html_logo_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png",
    html_favicon_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png"
)]

use std::fs;
use std::path::PathBuf;

use anyhow::{format_err, Context, Result};
use clap::Parser;
use relay_pii::{PiiAttachmentsProcessor, PiiConfig};

/// Apply data scrubbing (PII) rules on a minidump file.
///
/// Remove all heap memory:
///
///     {"applications": {"$heap_memory": ["@anything:remove"]}}
///
/// Remove all memory regions:
///
///     {"applications": {"$stack_memory || $heap_memory": ["@anything:remove"]}}
///
/// Remove credit cards from heap memory:
///
///     {"applications": {"$heap_memory": ["@creditcard:remove"]}}
///
/// For more information on how to scrub IP addresses, user file paths and how to define custom
/// regexes see <https://getsentry.github.io/relay/pii-config/>
#[derive(Debug, Parser)]
#[structopt(verbatim_doc_comment)]
struct Cli {
    /// Path to a PII config JSON file.
    #[arg(short, long)]
    config: PathBuf,

    /// Path to the minidump to rewrite.
    minidump: PathBuf,

    /// Optional output path. By default, the minidump file is overwritten.
    #[arg(short, long)]
    output: Option<PathBuf>,
}

impl Cli {
    fn load_pii_config(&self) -> Result<PiiConfig> {
        let json = fs::read_to_string(&self.config).with_context(|| "failed to read PII config")?;
        let config = serde_json::from_str(&json).with_context(|| "failed to parse PII config")?;
        Ok(config)
    }

    fn load_minidump(&self) -> Result<Vec<u8>> {
        let buf = fs::read(&self.minidump).with_context(|| "failed to open minidump")?;
        Ok(buf)
    }

    fn minidump_name(&self) -> &str {
        self.minidump
            .file_name()
            .and_then(|os_str| os_str.to_str())
            .unwrap_or_default()
    }

    fn write_output(&self, data: &[u8]) -> Result<()> {
        let path = match self.output {
            Some(ref output) => output,
            None => &self.minidump,
        };

        fs::write(path, data)
            .with_context(|| format!("failed to write minidump to {}", path.display()))?;

        println!("output written to {}", path.display());

        Ok(())
    }

    pub fn run(self) -> Result<()> {
        let config = self.load_pii_config()?;
        let processor = PiiAttachmentsProcessor::new(config.compiled());

        let mut data = self.load_minidump()?;
        let changed = processor
            .scrub_minidump(self.minidump_name(), &mut data)
            .map_err(|e| format_err!("{e}"))?; // does not implement std::error::Error

        if changed {
            self.write_output(&data)?;
        } else {
            println!("nothing changed.");
        }

        Ok(())
    }
}

fn print_error(error: &anyhow::Error) {
    eprintln!("Error: {error}");

    let mut cause = error.source();
    while let Some(ref e) = cause {
        eprintln!("  caused by: {e}");
        cause = e.source();
    }
}

fn main() {
    let cli = Cli::parse();

    match cli.run() {
        Ok(()) => (),
        Err(error) => {
            print_error(&error);
            std::process::exit(1);
        }
    }
}