fluidattacks-core 0.1.5

Fluid Attacks Core Library
Documentation
use anyhow::{Context, Result};
use base64::Engine;
use std::time::Duration;
use tokio::process::Command;

use super::types::{CredentialKind, Credentials, LsRemoteResult};

use super::aws;
use crate::git::clone::https::embed_credentials_in_url;
use crate::git::clone::ssh::{normalize_ssh_url, setup_ssh_env};

pub async fn ls_remote(
    url: &str,
    branch: &str,
    credentials: Option<&Credentials>,
    follow_redirects: bool,
) -> Result<LsRemoteResult> {
    match credentials.map(|c| &c.kind) {
        Some(CredentialKind::Ssh { key }) => {
            ls_remote_ssh(url, branch, key, follow_redirects).await
        }
        Some(CredentialKind::AwsRole { arn, external_id }) => {
            ls_remote_codecommit(url, branch, arn, external_id).await
        }
        Some(CredentialKind::Https { user, password }) => {
            let repo_url = embed_credentials_in_url(url, user, password)?;
            ls_remote_https(&repo_url, branch, follow_redirects, &[]).await
        }
        Some(CredentialKind::Token {
            token,
            is_pat,
            oauth_type: _,
        }) => {
            if *is_pat {
                let encoded = base64::engine::general_purpose::STANDARD.encode(format!(":{token}"));
                let extra_config = vec![
                    "-c".to_string(),
                    format!("http.extraHeader=Authorization: Basic {encoded}"),
                ];
                ls_remote_https(url, branch, follow_redirects, &extra_config).await
            } else {
                let user = if url.to_lowercase().contains("bitbucket") {
                    "x-token-auth"
                } else {
                    "oauth2"
                };
                let repo_url = embed_credentials_in_url(url, user, token)?;
                ls_remote_https(&repo_url, branch, follow_redirects, &[]).await
            }
        }
        None => ls_remote_https(url, branch, follow_redirects, &[]).await,
    }
}

async fn ls_remote_ssh(
    url: &str,
    branch: &str,
    key_b64: &str,
    follow_redirects: bool,
) -> Result<LsRemoteResult> {
    let key_data = base64::engine::general_purpose::STANDARD
        .decode(key_b64)
        .context("decoding SSH key")?;

    let key_file = tempfile::Builder::new()
        .prefix("melts-ssh-key-")
        .tempfile()
        .context("creating SSH key file")?;

    std::fs::write(key_file.path(), &key_data).context("writing SSH key")?;

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(key_file.path(), std::fs::Permissions::from_mode(0o400))
            .context("setting SSH key permissions")?;
    }

    let git_ssh_command = setup_ssh_env(key_file.path());
    let normalized_url = normalize_ssh_url(url);

    let mut args: Vec<String> = Vec::new();
    if follow_redirects {
        args.extend(["-c".to_string(), "http.followRedirects=true".to_string()]);
    }
    args.extend([
        "ls-remote".to_string(),
        "--".to_string(),
        normalized_url,
        branch.to_string(),
    ]);

    let output = tokio::time::timeout(
        Duration::from_secs(20),
        Command::new("git")
            .args(&args)
            .env("GIT_SSH_COMMAND", &git_ssh_command)
            .output(),
    )
    .await
    .context("git ls-remote timed out")?
    .context("running git ls-remote (SSH)")?;

    parse_ls_remote_output(&output)
}

async fn ls_remote_https(
    url: &str,
    branch: &str,
    follow_redirects: bool,
    extra_config: &[String],
) -> Result<LsRemoteResult> {
    let mut args = vec!["-c".to_string(), "http.sslVerify=false".to_string()];

    if follow_redirects {
        args.extend(["-c".to_string(), "http.followRedirects=true".to_string()]);
    }

    args.extend(extra_config.iter().cloned());
    args.extend([
        "ls-remote".to_string(),
        "--".to_string(),
        url.to_string(),
        branch.to_string(),
    ]);

    let output = tokio::time::timeout(
        Duration::from_secs(20),
        Command::new("git").args(&args).output(),
    )
    .await
    .context("git ls-remote timed out")?
    .context("running git ls-remote (HTTPS)")?;

    parse_ls_remote_output(&output)
}

async fn ls_remote_codecommit(
    url: &str,
    branch: &str,
    arn: &str,
    external_id: &str,
) -> Result<LsRemoteResult> {
    let creds = aws::assume_role(arn, external_id, url).await?;

    let args = vec![
        "-c".to_string(),
        "http.sslVerify=false".to_string(),
        "ls-remote".to_string(),
        "--".to_string(),
        url.to_string(),
        branch.to_string(),
    ];

    let output = tokio::time::timeout(
        Duration::from_secs(20),
        Command::new("git")
            .args(&args)
            .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)
            .output(),
    )
    .await
    .context("git ls-remote timed out")?
    .context("running git ls-remote (CodeCommit)")?;

    parse_ls_remote_output(&output)
}

fn parse_ls_remote_output(output: &std::process::Output) -> Result<LsRemoteResult> {
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
        return Ok(LsRemoteResult {
            commit: None,
            error: Some(stderr),
        });
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let commit = stdout
        .lines()
        .next()
        .and_then(|line| line.split('\t').next())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty());

    Ok(LsRemoteResult {
        commit,
        error: None,
    })
}