Skip to main content

fluidattacks_core/git/
remote.rs

1use anyhow::{Context, Result};
2use base64::Engine;
3use std::time::Duration;
4use tokio::process::Command;
5
6use crate::types::{CredentialKind, Credentials, LsRemoteResult};
7
8use crate::aws;
9use crate::git::clone::https::embed_credentials_in_url;
10use crate::git::clone::ssh::{normalize_ssh_url, setup_ssh_env};
11
12pub async fn ls_remote(
13    url: &str,
14    branch: &str,
15    credentials: Option<&Credentials>,
16    follow_redirects: bool,
17) -> Result<LsRemoteResult> {
18    match credentials.map(|c| &c.kind) {
19        Some(CredentialKind::Ssh { key }) => {
20            ls_remote_ssh(url, branch, key, follow_redirects).await
21        }
22        Some(CredentialKind::AwsRole { arn, external_id }) => {
23            ls_remote_codecommit(url, branch, arn, external_id).await
24        }
25        Some(CredentialKind::Https { user, password }) => {
26            let repo_url = embed_credentials_in_url(url, user, password)?;
27            ls_remote_https(&repo_url, branch, follow_redirects, &[]).await
28        }
29        Some(CredentialKind::Token {
30            token,
31            is_pat,
32            oauth_type: _,
33        }) => {
34            if *is_pat {
35                let encoded = base64::engine::general_purpose::STANDARD.encode(format!(":{token}"));
36                let extra_config = vec![
37                    "-c".to_string(),
38                    format!("http.extraHeader=Authorization: Basic {encoded}"),
39                ];
40                ls_remote_https(url, branch, follow_redirects, &extra_config).await
41            } else {
42                let user = if url.to_lowercase().contains("bitbucket") {
43                    "x-token-auth"
44                } else {
45                    "oauth2"
46                };
47                let repo_url = embed_credentials_in_url(url, user, token)?;
48                ls_remote_https(&repo_url, branch, follow_redirects, &[]).await
49            }
50        }
51        None => ls_remote_https(url, branch, follow_redirects, &[]).await,
52    }
53}
54
55async fn ls_remote_ssh(
56    url: &str,
57    branch: &str,
58    key_b64: &str,
59    follow_redirects: bool,
60) -> Result<LsRemoteResult> {
61    let key_data = base64::engine::general_purpose::STANDARD
62        .decode(key_b64)
63        .context("decoding SSH key")?;
64
65    let key_file = tempfile::Builder::new()
66        .prefix("melts-ssh-key-")
67        .tempfile()
68        .context("creating SSH key file")?;
69
70    std::fs::write(key_file.path(), &key_data).context("writing SSH key")?;
71
72    #[cfg(unix)]
73    {
74        use std::os::unix::fs::PermissionsExt;
75        std::fs::set_permissions(key_file.path(), std::fs::Permissions::from_mode(0o400))
76            .context("setting SSH key permissions")?;
77    }
78
79    let git_ssh_command = setup_ssh_env(key_file.path());
80    let normalized_url = normalize_ssh_url(url);
81
82    let mut args: Vec<String> = Vec::new();
83    if follow_redirects {
84        args.extend(["-c".to_string(), "http.followRedirects=true".to_string()]);
85    }
86    args.extend([
87        "ls-remote".to_string(),
88        "--".to_string(),
89        normalized_url,
90        branch.to_string(),
91    ]);
92
93    let output = tokio::time::timeout(
94        Duration::from_secs(20),
95        Command::new("git")
96            .args(&args)
97            .env("GIT_SSH_COMMAND", &git_ssh_command)
98            .output(),
99    )
100    .await
101    .context("git ls-remote timed out")?
102    .context("running git ls-remote (SSH)")?;
103
104    parse_ls_remote_output(&output)
105}
106
107async fn ls_remote_https(
108    url: &str,
109    branch: &str,
110    follow_redirects: bool,
111    extra_config: &[String],
112) -> Result<LsRemoteResult> {
113    let mut args = vec!["-c".to_string(), "http.sslVerify=false".to_string()];
114
115    if follow_redirects {
116        args.extend(["-c".to_string(), "http.followRedirects=true".to_string()]);
117    }
118
119    args.extend(extra_config.iter().cloned());
120    args.extend([
121        "ls-remote".to_string(),
122        "--".to_string(),
123        url.to_string(),
124        branch.to_string(),
125    ]);
126
127    let output = tokio::time::timeout(
128        Duration::from_secs(20),
129        Command::new("git").args(&args).output(),
130    )
131    .await
132    .context("git ls-remote timed out")?
133    .context("running git ls-remote (HTTPS)")?;
134
135    parse_ls_remote_output(&output)
136}
137
138async fn ls_remote_codecommit(
139    url: &str,
140    branch: &str,
141    arn: &str,
142    external_id: &str,
143) -> Result<LsRemoteResult> {
144    let creds = aws::assume_role(arn, external_id, url).await?;
145
146    let args = vec![
147        "-c".to_string(),
148        "http.sslVerify=false".to_string(),
149        "ls-remote".to_string(),
150        "--".to_string(),
151        url.to_string(),
152        branch.to_string(),
153    ];
154
155    let output = tokio::time::timeout(
156        Duration::from_secs(20),
157        Command::new("git")
158            .args(&args)
159            .env("AWS_ACCESS_KEY_ID", &creds.access_key)
160            .env("AWS_SECRET_ACCESS_KEY", &creds.secret_key)
161            .env("AWS_SESSION_TOKEN", &creds.session_token)
162            .env("AWS_DEFAULT_REGION", &creds.region)
163            .output(),
164    )
165    .await
166    .context("git ls-remote timed out")?
167    .context("running git ls-remote (CodeCommit)")?;
168
169    parse_ls_remote_output(&output)
170}
171
172fn parse_ls_remote_output(output: &std::process::Output) -> Result<LsRemoteResult> {
173    if !output.status.success() {
174        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
175        return Ok(LsRemoteResult {
176            commit: None,
177            error: Some(stderr),
178        });
179    }
180
181    let stdout = String::from_utf8_lossy(&output.stdout);
182    let commit = stdout
183        .lines()
184        .next()
185        .and_then(|line| line.split('\t').next())
186        .map(|s| s.trim().to_string())
187        .filter(|s| !s.is_empty());
188
189    Ok(LsRemoteResult {
190        commit,
191        error: None,
192    })
193}