spytrap-adb 0.3.5

Test a phone for stalkerware using adb and usb debugging to scan for suspicious apps and configuration
Documentation
use crate::errors::*;
use chrono::{DateTime, offset::Utc};
use serde::{Deserialize, Serialize};
use std::time::Duration;

const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);

const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
const READ_TIMEOUT: Duration = Duration::from_secs(30);

#[derive(Debug, Clone)]
pub struct Client {
    http: reqwest::Client,
}

impl Client {
    pub fn new() -> Result<Self> {
        let http = reqwest::Client::builder()
            .user_agent(APP_USER_AGENT)
            .connect_timeout(CONNECT_TIMEOUT)
            .read_timeout(READ_TIMEOUT)
            .build()
            .context("Failed to setup http client")?;
        Ok(Self { http })
    }

    pub async fn get(&self, url: &str) -> Result<reqwest::Response> {
        let req = self
            .http
            .get(url)
            .send()
            .await
            .context("Failed to send HTTP request")?;

        let status = req.status();
        let headers = req.headers();
        trace!("Received response from server: status={status:?}, headers={headers:?}");

        let req = req.error_for_status()?;
        Ok(req)
    }

    pub async fn github_branch_metadata(&self, base_url: &str, branch: &str) -> Result<GithubRef> {
        let url = format!("{}/{}", base_url, branch);

        info!("Fetching git repository meta data: {url:?}...");
        let metadata = self
            .get(&url)
            .await?
            .json::<GithubBranch>()
            .await
            .context("Failed to receive http response")?;

        let commit = metadata.commit;
        debug!("Found github commit for branch {branch:?}: {commit:?}");
        Ok(commit)
    }

    pub async fn github_download_file(
        &self,
        base_url: &str,
        commit: &GithubRef,
        filename: &str,
    ) -> Result<String> {
        let url = base_url
            .replace("{{commit}}", &commit.sha)
            .replace("{{filename}}", filename);
        info!("Downloading IOC file from {url:?}...");
        let body = self
            .get(&url)
            .await?
            .text()
            .await
            .context("Failed to download HTTP response")?;
        Ok(body)
    }
}

#[derive(Debug, Deserialize, Serialize)]
pub struct GithubBranch {
    pub name: String,
    pub commit: GithubRef,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct GithubRef {
    pub sha: String,
    pub commit: GithubCommit,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct GithubCommit {
    pub committer: GithubGitAuthor,
}

impl GithubCommit {
    pub fn release_timestamp(&self) -> Result<i64> {
        let datetime = &self.committer.date;
        let timestamp = parse_datetime(datetime)
            .with_context(|| anyhow!("Failed to parse datetime from github: {datetime:?}"))?;
        Ok(timestamp)
    }
}

#[derive(Debug, Deserialize, Serialize)]
pub struct GithubGitAuthor {
    pub name: String,
    pub email: String,
    pub date: String,
}

fn parse_datetime(datetime: &str) -> Result<i64> {
    let dt = DateTime::parse_from_rfc3339(datetime)?;
    let dt = DateTime::<Utc>::from(dt);
    Ok(dt.timestamp())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_datetime() {
        let timestamp = parse_datetime("2024-07-02T23:34:14Z").unwrap();
        assert_eq!(timestamp, 1719963254);
    }
}