fluidattacks-core-git 0.68.0

Fluid Attacks Core Git
Documentation
use std::path::Path;
use tokio::process::Command;

use super::{build_clone_args, reason, redirect_config, stall_config, CloneOpts};
use crate::aws::{self, AwsTempCreds};
use crate::errors::{CloneError, GitCallError};
use crate::types::CredentialKind;

pub async fn clone_codecommit(opts: &CloneOpts, dest: &Path) -> anyhow::Result<()> {
    let Some(CredentialKind::AwsRole {
        arn,
        external_id,
        via,
    }) = opts.credentials.as_ref().map(|c| &c.kind)
    else {
        anyhow::bail!("expected AwsRole credentials for CodeCommit clone");
    };
    Ok(run_git_clone(opts, dest, arn, external_id, via.as_deref()).await?)
}

pub(super) async fn run_git_clone(
    opts: &CloneOpts,
    dest: &Path,
    arn: &str,
    external_id: &str,
    via: Option<&str>,
) -> Result<(), CloneError> {
    let creds = aws::assume_role(arn, external_id, &opts.repo_url, via).await?;
    Ok(clone_with(opts, dest, &creds).await?)
}

async fn clone_with(
    opts: &CloneOpts,
    dest: &Path,
    creds: &AwsTempCreds,
) -> Result<(), GitCallError> {
    let mut command = Command::new("git");
    command
        .args(clone_args(opts, dest))
        .env("AWS_ACCESS_KEY_ID", &creds.access_key)
        .env("AWS_SECRET_ACCESS_KEY", &creds.secret_key)
        .env("AWS_SESSION_TOKEN", &creds.session_token)
        .env("AWS_DEFAULT_REGION", &creds.region);
    reason::run(&mut command, &[&creds.secret_key, &creds.session_token]).await
}

fn clone_args(opts: &CloneOpts, dest: &Path) -> Vec<std::ffi::OsString> {
    let mut extra_config = redirect_config(opts.follow_redirects).to_vec();
    extra_config.extend(stall_config());
    build_clone_args(opts, &opts.repo_url, dest, &extra_config)
}

#[cfg(test)]
mod tests {
    use std::path::Path;

    use super::{clone_args, clone_with};
    use crate::aws::AwsTempCreds;
    use crate::clone::tests::opts;
    use crate::clone::{CloneOpts, CloneShape};
    use crate::errors::GitCallError;

    #[test]
    fn a_redirect_is_followed_only_when_the_root_asks_for_one() {
        for follow_redirects in [false, true] {
            let opts = CloneOpts {
                follow_redirects,
                ..opts(CloneShape::SingleBranch)
            };

            let args = clone_args(&opts, Path::new("/tmp/a_root"));

            let config = format!("http.followRedirects={follow_redirects}");
            assert!(args.windows(2).any(|pair| pair == ["-c", config.as_str()]));
        }
    }

    #[test]
    fn git_checks_the_certificate() {
        let args = clone_args(&opts(CloneShape::SingleBranch), Path::new("/tmp/a_root"));

        assert!(!args.iter().any(|arg| arg == "http.sslVerify=false"));
    }

    #[tokio::test]
    async fn the_session_stays_out_of_what_git_says() {
        let tmp = tempfile::tempdir().unwrap();
        // git names the repository it could not find, so the url carries the session into stderr.
        let opts = CloneOpts {
            repo_url: format!("{}/a-secret-key/a-token", tmp.path().display()),
            ..opts(CloneShape::SingleBranch)
        };
        let creds = AwsTempCreds {
            access_key: "an-access-key".to_owned(),
            secret_key: "a-secret-key".to_owned(),
            session_token: "a-token".to_owned(),
            region: "us-east-1".to_owned(),
        };

        let outcome = clone_with(&opts, &tmp.path().join("a_root"), &creds).await;

        let Err(GitCallError::Refused { stderr, .. }) = outcome else {
            panic!("{outcome:?}");
        };
        assert!(stderr.contains("[REDACTED]/[REDACTED]"), "{stderr}");
    }

    #[test]
    fn a_clone_that_stops_receiving_data_is_cut_after_ten_minutes() {
        let args = clone_args(&opts(CloneShape::SingleBranch), Path::new("/tmp/a_root"));

        assert!(args
            .windows(2)
            .any(|pair| pair == ["-c", "http.lowSpeedLimit=1000"]));
        assert!(args
            .windows(2)
            .any(|pair| pair == ["-c", "http.lowSpeedTime=600"]));
    }
}