Repository: bottlerocket-os/bottlerocket
File: sources/logdog/src/log_request.rs (Rust)

Code before:
        "settings" => handle_settings_request(&req, tempdir).await?,
        "exec" => handle_exec_request(&req, tempdir)?,
        "http" | "https" => handle_http_request(&req, tempdir)?,
        "file" => handle_file_request(&req, tempdir)?,
        "glob" => handle_glob_request(&req, tempdir)?,
        unmatched => {
            return Err(error::Error::UnhandledRequest {
                mode: unmatched.into(),
                request: request.into(),
            })
        }
    }
    Ok(())
}

pub(crate) fn normalize(
    json: &serde_json::Value,
    prefix: String,
    flattened: &mut HashMap<String, serde_json::Value>,
) -> Result<()> {
    match json {
        serde_json::Value::Object(map) => {
            for (k, v) in map {
                let new_key = if prefix.is_empty() {
                    k.clone()
                } else {
                    format!("{}.{}", prefix, k)
                };
                normalize(v, new_key, flattened)?;
            }
        }
        _ => {
            flattened.insert(prefix, json.clone());
        }
    };
    Ok(())
}

/// Unflatten a vector of key-value pairs into a JSON object
pub(crate) fn denormalize(
    flattened: HashMap<String, serde_json::Value>,
) -> Result<serde_json::Value> {
    let mut result = serde_json::Value::Object(serde_json::Map::new());
    for (k, v) in flattened {
        let parts = k.split('.');
        let mut current = &mut result;
        for part in parts {
            if let serde_json::Value::Object(map) = current {
                current = map
                    .entry(part.to_string())
                    .or_insert(serde_json::Value::Object(serde_json::Map::new()));

Review comment (suggestion):
The use of `.` as a separator is ambiguous because some keys have dots in them. `settings.kernel.sysctl` is one but there are many others. This approach would corrupt the structure when unflattened.

I would look instead at `serde_json::Value` and [pointer_mut](https://docs.rs/serde_json/latest/serde_json/value/enum.Value.html#method.pointer_mut) which takes a JSONPointer query. Then you could replace the value with "REDACTED" if it is a string.

Code after:
        "exec" => handle_exec_request(&req, tempdir)?,
        "http" | "https" => handle_http_request(&req, tempdir)?,
        "file" => handle_file_request(&req, tempdir)?,
        "glob" => handle_glob_request(&req, tempdir)?,
        unmatched => {
            return Err(error::Error::UnhandledRequest {
                mode: unmatched.into(),
                request: request.into(),
            })
        }
    }
    Ok(())
}

/// Strips user-data out of the settings model
fn strip_user_data(input: &mut serde_json::Value) {
    let redacted = serde_json::Value::String("REDACTED".into());
    match input {
        serde_json::Value::Object(map) => {
            for (key, value) in map.iter_mut() {
                if key == "user-data" {
                    *value = redacted.clone();
                } else {
                    strip_user_data(value)
                }
            }
        }
        serde_json::Value::Array(array) => {
            for child in array {
                strip_user_data(child)
            }
        }
        _ => {}
    }
}

/// Requests settings from the API, filters them, and writes the output to `tempdir`
async fn handle_settings_request<P>(request: &LogRequest<'_>, tempdir: P) -> Result<()>
where
    P: AsRef<Path>,
{
    let mut settings = get_settings().await?;

    // Filter all settings that match any of the "sensitive" patterns
    let redacted = serde_json::Value::String("REDACTED".into());
    for pattern in SENSITIVE_SETTINGS_PATTERNS {
        if let Some(found) = settings.pointer_mut(pattern) {
            *found = redacted.clone();
        }
    }
    strip_user_data(&mut settings);