diff --git a/src/action.rs b/src/action.rs index b71c62c..d6761ff 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,4 +1,8 @@ #![allow(dead_code)] +//! Action module for defining and handling the GitHub Action's inputs, outputs, and core functionality +//! +//! This module contains the Action struct which represents the GitHub Action and implements +//! the necessary functionality to process inputs, validate configurations, and manage outputs. use std::path::PathBuf; use anyhow::{Context, Result}; @@ -6,12 +10,17 @@ use ghactions::prelude::*; use ghactions_core::repository::reference::RepositoryReference as Repository; use ghastoolkit::{CodeQL, CodeQLPack, codeql::CodeQLLanguage}; +/// ASCII art banner for the CodeQL Extractor Action pub const BANNER: &str = r#" ___ _ ____ __ __ _ _ _ / __\___ __| | ___ /___ \/ / /__\_ _| |_ /_\ ___| |_ / / / _ \ / _` |/ _ \// / / / /_\ \ \/ / __|//_\\ / __| __| / /__| (_) | (_| | __/ \_/ / /___//__ > <| |_/ _ \ (__| |_ \____/\___/ \__,_|\___\___,_\____/\__/ /_/\_\\__\_/ \_/\___|\__|"#; + +/// Version of the CodeQL Extractor Action, pulled from Cargo.toml pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Authors of the CodeQL Extractor Action, pulled from Cargo.toml pub const AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); /// This action is for 3rd party CodeQL extractors to be used in GitHub Actions @@ -94,6 +103,22 @@ pub struct Action { } impl Action { + /// Returns the GitHub Token for the action + pub fn get_token(&self) -> String { + if self.token.is_empty() { + std::env::var("GITHUB_TOKEN").unwrap_or_default() + } else { + self.token.clone() + } + } + + /// Returns the working directory for the action + /// + /// If no working directory is provided, the current directory is used. + /// Otherwise, the provided directory is resolved to an absolute path. + /// + /// # Returns + /// - `Result`: The resolved working directory path pub fn working_directory(&self) -> Result { if self.working_directory.is_empty() { log::debug!("No working directory provided, using the current directory"); @@ -108,34 +133,102 @@ impl Action { )) } - /// Gets the repository to use for the extractor. If the repository is not provided, - /// it will use the repository that the action is running in. + /// Gets the repository references for the extractors + /// + /// If no extractor repositories are provided, the current repository is used. + /// Otherwise, the provided repositories are parsed into Repository objects. + /// + /// # Returns + /// - `Result>`: A list of parsed repository references pub fn extractor_repository(&self) -> Result> { if self.extractors.is_empty() { log::debug!("No extractor repository provided, using the current repository"); return Ok(vec![Repository::parse(&self.get_repository()?)?]); } - log::debug!("Using the provided extractor repository"); + log::debug!( + "Using the provided extractor repositories: {:?}", + self.extractors + ); - Ok(self + let repos: Vec = self .extractors .iter() - .filter_map(|ext| { - Repository::parse(ext) - .context(format!("Failed to parse extractor repository `{ext}`")) - .ok() + .filter_map(|ext| match Repository::parse(ext) { + Ok(repo) => { + log::debug!( + "Successfully parsed repository: {} / {}", + repo.owner, + repo.name + ); + Some(repo) + } + Err(e) => { + log::warn!("Failed to parse extractor repository `{}`: {}", ext, e); + None + } }) - .collect::>()) + .collect(); + + log::debug!("Parsed {} repositories", repos.len()); + Ok(repos) } + /// Returns the list of languages to use for CodeQL analysis. pub fn languages(&self) -> Vec { - self.languages + log::debug!("Getting languages for analysis: {:?}", self.languages); + let languages = self + .languages .iter() .map(|lang| CodeQLLanguage::from(lang.as_str())) - .collect() + .collect(); + log::debug!("Converted to CodeQL languages: {:?}", languages); + languages + } + + /// Gets the possible directories for CodeQL operations. + /// + /// This function identifies potential locations for CodeQL operation directories in the following order: + /// 1. The `.codeql` directory in the GitHub workspace (if running in GitHub Actions) + /// 2. The `.codeql` directory in the current working directory + /// 3. The `.codeql` directory in the GitHub Actions runner's temp directory (if available) + /// 4. The `.codeql` directory in the system's temporary directory + /// + /// Each path is checked for existence and created if necessary by the caller. + /// + /// # Returns + /// - `Result>`: A vector of possible directory paths for CodeQL operations + /// + /// # Errors + /// - If `working_directory()` fails + /// - If path canonicalization fails + fn get_codeql_directories(&self) -> Result> { + let mut paths = Vec::new(); + + // GITHUB_WORKSPACE + if let Ok(github_workspace) = std::env::var("GITHUB_WORKSPACE") { + paths.push(PathBuf::from(github_workspace).join(".codeql")); + } + + // Local CodeQL directory in the working directory + if let Ok(local_codeql) = self.working_directory()?.join(".codeql").canonicalize() { + paths.push(local_codeql); + } + + // Runner temp directory + if let Ok(runner_temp) = std::env::var("RUNNER_TEMP") { + paths.push(PathBuf::from(runner_temp).join(".codeql").canonicalize()?); + } + // temp_dir + if let Ok(temp_dir) = std::env::temp_dir().canonicalize() { + paths.push(temp_dir.join(".codeql")); + } + + Ok(paths) } + /// Returns the directory to use for CodeQL operations. + /// /// Gets the CodeQL directory to use for the action. It will first check if a local /// `.codeql` directory exists in the working directory parent. If not, it will /// use the `RUNNER_TEMP` directory. If neither exists, it will create a new @@ -144,17 +237,8 @@ impl Action { /// It uses the parent of the working directory to to stop issues where the /// database/sarif files gets indexed by CodeQL. pub fn get_codeql_dir(&self) -> Result { - let paths = vec![ - // Local CodeQL directory in the working directory parent - self.working_directory()? - .join("..") - .join(".codeql") - .canonicalize()?, - // Runner temp directory - PathBuf::from(std::env::var("RUNNER_TEMP").unwrap_or_else(|_| "/tmp".to_string())) - .join(".codeql") - .canonicalize()?, - ]; + let paths = self.get_codeql_directories()?; + log::debug!("Possible CodeQL directories: {:?}", paths); for path in paths { if !path.exists() { @@ -173,6 +257,11 @@ impl Action { Err(anyhow::anyhow!("Failed to create CodeQL directory",)) } + /// Validates the provided languages against the supported CodeQL languages. + /// + /// # Errors + /// + /// Returns an error if any of the provided languages are not supported. pub fn validate_languages(&self, codeql_languages: &Vec) -> Result<()> { for lang in self.languages() { let mut supported = false; @@ -198,6 +287,9 @@ impl Action { Ok(()) } + /// Returns the CodeQL version to use. + /// + /// If the CodeQL version is not provided, it defaults to "latest". pub fn codeql_version(&self) -> &str { if self.codeql_version.is_empty() { log::debug!("No CodeQL version provided, using the latest version"); @@ -206,6 +298,11 @@ impl Action { &self.codeql_version } + /// Installs the specified CodeQL packs. + /// + /// # Errors + /// + /// Returns an error if any of the packs cannot be installed. pub async fn install_packs(&self, codeql: &CodeQL) -> Result<()> { log::info!("Installing CodeQL Packs"); for pack in &self.packs { @@ -238,11 +335,15 @@ impl Action { Ok(()) } + /// Returns whether attestation is enabled. pub fn attestation(&self) -> bool { + log::debug!("Attestation enabled: {}", self.attestation); self.attestation } + /// Returns whether empty databases are allowed. pub fn allow_empty_database(&self) -> bool { + log::debug!("Allow empty database: {}", self.allow_empty_database); self.allow_empty_database } } @@ -251,6 +352,12 @@ impl Action { mod tests { use super::*; + /// Helper function to create a test Action instance with predefined values + /// + /// Creates an Action with: + /// - A single extractor repository "owner/repo" + /// - A single language "iac" + /// - Default values for all other fields fn action() -> Action { Action { extractors: vec!["owner/repo".to_string()], @@ -259,6 +366,11 @@ mod tests { } } + /// Test that language validation works correctly + /// + /// Tests two scenarios: + /// 1. When a language is specified that isn't supported by CodeQL (should error) + /// 2. When a language is specified that is supported by CodeQL (should pass) #[test] fn test_validate_languages() { let action = action(); diff --git a/src/codeql.rs b/src/codeql.rs index 9930cce..1dba2db 100644 --- a/src/codeql.rs +++ b/src/codeql.rs @@ -1,29 +1,156 @@ +//! CodeQL installation and management utilities +//! +//! This module provides helper functions for downloading and installing CodeQL, +//! particularly through alternative methods like GitHub CLI when the standard +//! installation process fails. + use anyhow::{Context, Result}; +use ghactions::ActionTrait; use ghastoolkit::CodeQL; -/// Download the CodeQL CLI using the GitHub CLI -pub async fn gh_codeql_download(codeql_version: &str) -> Result { +use crate::action::Action; + +/// Download and install the CodeQL CLI, with fallback to GitHub CLI if necessary +pub async fn codeql_download(action: &Action) -> Result { + let token = action.get_token(); + + let mut codeql = CodeQL::init() + .build() + .await + .context("Failed to create CodeQL instance")?; + log::debug!("CodeQL :: {codeql:?}"); + + if !codeql.is_installed().await { + let codeql_version = action.codeql_version(); + log::info!("CodeQL not installed, installing `{codeql_version}`..."); + + // Try to install with authentication first (if token is available) + if !token.is_empty() { + let octocrab_auth = action.octocrab_with_token(token)?; + if let Ok(_) = codeql.install(&octocrab_auth, codeql_version).await { + log::info!("CodeQL installed using authentication"); + return Ok(codeql); + } else { + log::warn!( + "Failed to install CodeQL with authentication, trying without authentication..." + ); + } + } + + // Try to install without authentication + let octocrab = action.octocrab_without_token()?; + if let Ok(_) = codeql.install(&octocrab, codeql_version).await { + log::info!("CodeQL installed without authentication"); + return Ok(codeql); + } else { + log::warn!("Failed to install CodeQL without authentication"); + log::info!("Attempting to install CodeQL using GitHub CLI..."); + } + + let location = gh_codeql_download(codeql_version) + .await + .context("Failed to download CodeQL using GitHub CLI")?; + // Reinitialize CodeQL with the new path + codeql = CodeQL::init() + .path(location) + .build() + .await + .context("Failed to create CodeQL instance after GitHub CLI installation")?; + + log::info!("CodeQL installed"); + } else { + log::info!("CodeQL already installed"); + } + + Ok(codeql) +} + +/// Download and install the CodeQL CLI using the GitHub CLI +/// +/// This function serves as a fallback installation method when the standard CodeQL +/// installation process fails. It uses the GitHub CLI to: +/// 1. Install the gh-codeql extension +/// 2. Set the specified CodeQL version +/// 3. Install the CodeQL stub for command-line access +/// +/// # Arguments +/// * `codeql_version` - The version of CodeQL to download (e.g., "latest" or a specific version) +/// +/// # Returns +/// * `Result` - Path to the installed CodeQL binary or an error +async fn gh_codeql_download(codeql_version: &str) -> Result { log::info!("Downloading CodeQL Extension for GitHub CLI..."); - tokio::process::Command::new("gh") + log::debug!("Running command: gh extensions install github/gh-codeql"); + let status = tokio::process::Command::new("gh") .args(&["extensions", "install", "github/gh-codeql"]) + .env( + "GH_TOKEN", + std::env::var("GITHUB_TOKEN").unwrap_or_default(), + ) .status() .await .context("Failed to execute `gh extensions install github/gh-codeql` command")?; + if !status.success() { + log::error!( + "Failed to install GitHub CLI CodeQL extension. Exit code: {:?}", + status.code() + ); + return Err(anyhow::anyhow!( + "GitHub CLI CodeQL extension installation failed with exit code: {:?}", + status.code() + )); + } + log::debug!("GitHub CLI CodeQL extension installed successfully"); + log::info!("Setting CodeQL version to {codeql_version}..."); - tokio::process::Command::new("gh") + log::debug!("Running command: gh codeql set-version {codeql_version}"); + let status = tokio::process::Command::new("gh") .args(&["codeql", "set-version", codeql_version]) + .env( + "GH_TOKEN", + std::env::var("GITHUB_TOKEN").unwrap_or_default(), + ) .status() .await .context("Failed to execute `gh codeql set-version` command")?; - log::info!("Install CodeQL stub..."); - tokio::process::Command::new("gh") + if !status.success() { + log::error!( + "Failed to set CodeQL version. Exit code: {:?}", + status.code() + ); + return Err(anyhow::anyhow!( + "Setting CodeQL version failed with exit code: {:?}", + status.code() + )); + } + log::debug!("CodeQL version set to {codeql_version} successfully"); + + log::info!("Installing CodeQL stub..."); + log::debug!("Running command: gh codeql install-stub"); + let status = tokio::process::Command::new("gh") .args(&["codeql", "install-stub"]) + .env( + "GH_TOKEN", + std::env::var("GITHUB_TOKEN").unwrap_or_default(), + ) .status() .await .context("Failed to execute `gh codeql install-stub` command")?; + if !status.success() { + log::error!( + "Failed to install CodeQL stub. Exit code: {:?}", + status.code() + ); + return Err(anyhow::anyhow!( + "CodeQL stub installation failed with exit code: {:?}", + status.code() + )); + } + log::debug!("CodeQL stub installed successfully"); + let codeql = CodeQL::new().await; if codeql.is_installed().await { log::info!("CodeQL CLI installed successfully via GitHub CLI"); diff --git a/src/extractors.rs b/src/extractors.rs index cce63ef..b48cbce 100644 --- a/src/extractors.rs +++ b/src/extractors.rs @@ -4,22 +4,67 @@ use ghactions_core::repository::reference::RepositoryReference as Repository; use octocrab::models::repos::{Asset, Release}; use std::{os::unix::fs::PermissionsExt, path::PathBuf}; +/// Fetches a release from a GitHub repository +/// +/// If the repository reference includes a specific tag, it fetches that release. +/// Otherwise, it fetches the latest release. +/// +/// # Arguments +/// * `client` - The Octocrab client to use for API requests +/// * `repository` - The repository reference containing owner, name, and optional tag +/// +/// # Returns +/// * `Result` - The fetched release or an error async fn fetch_releases(client: &octocrab::Octocrab, repository: &Repository) -> Result { + log::debug!( + "Fetching releases for repository: {}/{}", + repository.owner, + repository.name + ); let release = if let Some(rel) = &repository.reference { log::info!("Fetching release by tag: {}", rel); - client + log::debug!( + "API call: repos/{}/{}/releases/tags/{}", + repository.owner, + repository.name, + rel + ); + match client .repos(repository.owner.clone(), repository.name.clone()) .releases() .get_by_tag(&rel) - .await? + .await + { + Ok(release) => release, + Err(e) => { + log::error!("Failed to fetch release by tag '{}': {}", rel, e); + return Err(anyhow::anyhow!( + "Failed to fetch release by tag '{}': {}", + rel, + e + )); + } + } } else { - log::info!("Fetching latest release",); + log::info!("Fetching latest release"); + log::debug!( + "API call: repos/{}/{}/releases/latest", + repository.owner, + repository.name + ); // Get Latest Release - client + match client .repos(repository.owner.clone(), repository.name.clone()) .releases() .get_latest() - .await? + .await + { + Ok(release) => release, + Err(e) => { + log::error!("Failed to fetch latest release: {}", e); + return Err(anyhow::anyhow!("Failed to fetch latest release: {}", e)); + } + } }; log::info!("Release :: {} - {:?}", release.tag_name, release.created_at); @@ -135,16 +180,24 @@ pub async fn fetch_extractor( } // Find `codeql-extractor.yml` in the extracted directory using glob - for glob in glob::glob( + log::debug!( + "Searching for codeql-extractor.yml in {}", + extractor_pack.display() + ); + if let Some(glob_result) = glob::glob( &extractor_pack .join("**/codeql-extractor.yml") .to_string_lossy(), - )? { - match glob { + )? + .next() + { + match glob_result { Ok(path) => { // TODO: Load and check the extractor configuration - log::debug!("Extractor Path :: {path:?}"); + log::debug!("Found extractor configuration at: {path:?}"); let full_path = path.parent().unwrap().to_path_buf().canonicalize()?; + log::debug!("Using extractor directory: {}", full_path.display()); + // Linux and Macos #[cfg(unix)] { @@ -154,31 +207,95 @@ pub async fn fetch_extractor( return Ok(full_path); } Err(e) => { - log::error!("Failed to find extractor: {e}"); - return Err(anyhow::anyhow!("Failed to find extractor: {e}")); + log::error!("Failed to access extractor path: {e}"); + return Err(anyhow::anyhow!("Failed to access extractor path: {e}")); } } + } else { + log::warn!( + "No codeql-extractor.yml found in {}", + extractor_pack.display() + ); } Ok(extractor_pack) } /// Update the SARIF file with the extractor information (CodeQL ${language}) /// -/// Update only the `runs.0.tool.driver` section of the SARIF file +/// Updates only the `runs.0.tool.driver` section of the SARIF file to include +/// information about which extractor was used. This helps in distinguishing +/// results from different CodeQL extractors when analyzing multiple languages. +/// +/// # Arguments +/// * `path` - Path to the SARIF file that needs to be updated +/// * `extractor` - Name of the extractor to be added to the SARIF metadata +/// +/// # Returns +/// * `Result<()>` - Success or an error if the SARIF file couldn't be updated pub fn update_sarif(path: &PathBuf, extractor: String) -> Result<()> { - let sarif_content = - std::fs::read_to_string(path).context(format!("Failed to read SARIF file: {:?}", path))?; - let mut sarif_json: serde_json::Value = serde_json::from_str(&sarif_content) - .context(format!("Failed to parse SARIF file: {:?}", path))?; + log::debug!( + "Updating SARIF file at {} with extractor information: {}", + path.display(), + extractor + ); + + // Read SARIF file + let sarif_content = match std::fs::read_to_string(path) { + Ok(content) => content, + Err(e) => { + log::error!("Failed to read SARIF file {}: {}", path.display(), e); + return Err(anyhow::anyhow!( + "Failed to read SARIF file: {:?} - {}", + path, + e + )); + } + }; + + // Parse SARIF JSON + let mut sarif_json: serde_json::Value = match serde_json::from_str(&sarif_content) { + Ok(json) => json, + Err(e) => { + log::error!( + "Failed to parse SARIF file {} as JSON: {}", + path.display(), + e + ); + return Err(anyhow::anyhow!( + "Failed to parse SARIF file: {:?} - {}", + path, + e + )); + } + }; + + log::debug!( + "SARIF structure: has runs={}, has results={}", + sarif_json.get("runs").is_some(), + sarif_json + .get("runs") + .and_then(|r| r.get(0)) + .and_then(|r| r.get("results")) + .is_some() + ); - log::debug!("SARIF JSON :: {sarif_json:#?}"); + // Update the tool driver name if let Some(tool) = sarif_json .get_mut("runs") .and_then(|runs| runs.get_mut(0)) .and_then(|run| run.get_mut("tool")) { if let Some(driver) = tool.get_mut("driver") { - driver["name"] = serde_json::Value::String(format!("CodeQL - {}", extractor)); + let new_name = format!("CodeQL - {}", extractor); + log::debug!( + "Updating tool.driver.name from '{}' to '{}'", + driver + .get("name") + .and_then(|n| n.as_str()) + .unwrap_or("unknown"), + new_name + ); + driver["name"] = serde_json::Value::String(new_name); log::info!("Updated SARIF file with extractor: {extractor}"); } else { log::warn!("No 'driver' field found in SARIF file"); @@ -187,14 +304,43 @@ pub fn update_sarif(path: &PathBuf, extractor: String) -> Result<()> { log::warn!("No 'runs' or 'tool' field found in SARIF file"); } - let data = serde_json::to_string(&sarif_json) - .context(format!("Failed to serialize SARIF JSON: {:?}", path))?; + // Serialize and write back to file + let data = match serde_json::to_string(&sarif_json) { + Ok(json) => json, + Err(e) => { + log::error!("Failed to serialize updated SARIF JSON: {}", e); + return Err(anyhow::anyhow!( + "Failed to serialize SARIF JSON: {:?} - {}", + path, + e + )); + } + }; + // Write the updated SARIF back to the file - std::fs::write(path, data).context(format!("Failed to write SARIF file: {:?}", path))?; + if let Err(e) = std::fs::write(path, &data) { + log::error!("Failed to write updated SARIF file: {}", e); + return Err(anyhow::anyhow!( + "Failed to write SARIF file: {:?} - {}", + path, + e + )); + } + + log::debug!("Successfully updated SARIF file at {}", path.display()); Ok(()) } -/// Update the permissions for tool scripts (*.sh) and the extractor (extractor) +/// Update the permissions for tool scripts (*.sh) and the extractor executables +/// +/// Makes shell scripts and extractor binaries executable by setting appropriate permissions. +/// Looks for tools in standard locations for Linux (linux64/extractor) and macOS (osx64/extractor). +/// +/// # Arguments +/// * `path` - The base path where tools are located +/// +/// # Returns +/// * `Result<()>` - Success or an error if permissions couldn't be set fn update_tools_permisisons(path: &PathBuf) -> Result<()> { let tools_path = path.join("tools"); log::info!("Tools :: {tools_path:?}"); @@ -226,10 +372,45 @@ fn update_tools_permisisons(path: &PathBuf) -> Result<()> { Ok(()) } -/// Sets the file permissions to be executable +/// Sets the file permissions to be executable (read and execute for all users) +/// +/// Sets the permissions to 0o555 (r-xr-xr-x) which allows reading and +/// execution by all users, but no write permissions. +/// +/// # Arguments +/// * `path` - The path to the file whose permissions should be set +/// +/// # Returns +/// * `Result<()>` - Success or an error if permissions couldn't be set fn set_permissions(path: &PathBuf) -> Result<()> { log::info!("Setting permissions for :: {:?}", path); + + // Get current permissions for logging + if let Ok(metadata) = std::fs::metadata(path) { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + log::debug!("Current permissions: {:o}", metadata.permissions().mode()); + } + } else { + log::warn!("Could not get current file metadata for {}", path.display()); + } + + log::debug!("Setting permissions to 0o555 (r-xr-xr-x)"); let perms = std::fs::Permissions::from_mode(0o555); - std::fs::set_permissions(&path, perms)?; - Ok(()) + + match std::fs::set_permissions(&path, perms) { + Ok(_) => { + log::debug!("Successfully set permissions for {}", path.display()); + Ok(()) + } + Err(e) => { + log::error!("Failed to set permissions for {}: {}", path.display(), e); + Err(anyhow::anyhow!( + "Failed to set permissions for {}: {}", + path.display(), + e + )) + } + } } diff --git a/src/main.rs b/src/main.rs index de74384..4743e19 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,7 @@ +//! Main module for the CodeQL Extractor Action +//! +//! This module contains the main function and orchestrates the entire workflow +//! for setting up CodeQL, fetching and configuring extractors, and running analyses. use anyhow::{Context, Result}; use ghactions::{ActionTrait, group, groupend}; use ghactions_core::RepositoryReference; @@ -9,10 +13,18 @@ mod action; mod codeql; mod extractors; +use crate::codeql::codeql_download; use action::{AUTHORS, Action, BANNER, VERSION}; -use crate::codeql::gh_codeql_download; - +/// Main function that drives the CodeQL Extractor Action workflow +/// +/// This function: +/// 1. Initializes the Action +/// 2. Sets up CodeQL +/// 3. Fetches and configures the extractors +/// 4. Creates databases for each language +/// 5. Runs analyses on the databases +/// 6. Processes and updates the SARIF results #[tokio::main] async fn main() -> Result<()> { let mut action = Action::init()?; @@ -28,42 +40,23 @@ async fn main() -> Result<()> { let cwd = action .working_directory() .context("Failed to get working directory")?; - let codeql_dir = action.get_codeql_dir()?; + let codeql_dir = action + .get_codeql_dir() + .context("Failed to get CodeQL directory")?; let databases = codeql_dir.join("databases"); let sarif_output = codeql_dir.join("results"); group!("Setting up CodeQL"); - let mut codeql = CodeQL::init() - .build() + let mut codeql = codeql_download(&action) .await - .context("Failed to create CodeQL instance")?; - log::debug!("CodeQL :: {codeql:?}"); - - if !codeql.is_installed().await { - let codeql_version = action.codeql_version(); - log::info!("CodeQL not installed, installing `{codeql_version}`..."); - - if let Err(error) = codeql.install(&octocrab, codeql_version).await { - log::warn!("Failed to install CodeQL: {error:?}"); - log::info!("Attempting to install CodeQL using GitHub CLI..."); - - let location = gh_codeql_download(codeql_version) - .await - .context("Failed to download CodeQL using GitHub CLI")?; - - codeql = CodeQL::init() - .path(location) - .build() - .await - .context("Failed to create CodeQL instance after GitHub CLI installation")?; - } + .context("Failed to set up CodeQL")?; + log::info!( + "CodeQL CLI Version :: {}", + codeql.version().unwrap_or_default() + ); - log::info!("CodeQL installed"); - } else { - log::info!("CodeQL already installed"); - } // Packs installation action.install_packs(&codeql).await?; @@ -82,6 +75,10 @@ async fn main() -> Result<()> { info!("Created Extractor Directory :: {extractor_path:?}"); } + log::debug!( + "Creating extractors container for {} repositories", + extractor_repos.len() + ); let mut extractors: Vec<(CodeQLExtractor, RepositoryReference)> = Vec::new(); for extractor_repo in extractor_repos.iter() { @@ -90,23 +87,60 @@ async fn main() -> Result<()> { extractor_repo.owner, extractor_repo.name ); + log::debug!("Repository reference details: {:?}", extractor_repo); - let extractor_path = extractors::fetch_extractor( + let extractor_path = match extractors::fetch_extractor( &octocrab, extractor_repo, action.attestation(), &extractor_path, ) .await - .context("Failed to fetch extractor")?; + { + Ok(path) => { + log::debug!("Successfully fetched extractor to {}", path.display()); + path + } + Err(e) => { + log::error!( + "Failed to fetch extractor from {}/{}: {}", + extractor_repo.owner, + extractor_repo.name, + e + ); + return Err(e).context("Failed to fetch extractor"); + } + }; log::info!("Extractor :: {extractor_path:?}"); + log::debug!( + "Appending search path to CodeQL instance: {}", + extractor_path.display() + ); codeql.append_search_path(&extractor_path); - log::info!("Extractor Path :: {extractor_path:?}"); - let extractor = - CodeQLExtractor::load_path(extractor_path).context("Failed to load extractor")?; + log::info!("Loading CodeQL extractor from path: {extractor_path:?}"); + let extractor = match CodeQLExtractor::load_path(extractor_path.clone()) { + Ok(ext) => { + log::debug!( + "Successfully loaded extractor: name={}, version={}, languages={:?}", + ext.name, + ext.version, + ext.languages() + ); + ext + } + Err(e) => { + log::error!( + "Failed to load extractor from {}: {}", + extractor_path.display(), + e + ); + return Err(anyhow::anyhow!("Failed to load extractor: {}", e)); + } + }; + log::debug!("Adding extractor to collection"); extractors.push((extractor, extractor_repo.clone())); } @@ -159,17 +193,33 @@ async fn main() -> Result<()> { .build() .context("Failed to create database")?; - log::info!("Creating database..."); + log::info!("Creating CodeQL database for language: {}", language); + log::debug!( + "Database creation parameters for: {}", + database_path.display() + ); + + let start_time = std::time::Instant::now(); match codeql.database(&database).overwrite().create().await { Ok(_) => { - log::debug!("Created database :: {database:?}"); + let elapsed = start_time.elapsed(); + log::debug!("Successfully created database :: {database:?}"); + log::info!( + "Database creation completed in {:.2} seconds", + elapsed.as_secs_f64() + ); } Err(e) => { log::error!("Failed to create database: {e:?}"); + log::debug!("Database creation error details: {:?}", e); + if action.allow_empty_database() { - log::warn!("Allowing empty database"); + log::warn!( + "Empty database allowed by configuration, continuing with next language" + ); continue; } else { + log::error!("Empty database not allowed, aborting"); return Err(anyhow::anyhow!("Failed to create database: {e:?}")); } } @@ -184,6 +234,15 @@ async fn main() -> Result<()> { group!(format!("Running {language} analysis")); + log::info!("Starting CodeQL analysis for language: {}", language); + log::debug!( + "Analysis configuration: database={}, queries={:?}, output={}", + database_path.display(), + queries, + sarif_path.display() + ); + + let analysis_start_time = std::time::Instant::now(); match codeql .database(&database) .queries(queries) @@ -192,13 +251,17 @@ async fn main() -> Result<()> { .await { Ok(_) => { - log::info!("Analysis complete"); + let elapsed = analysis_start_time.elapsed(); + log::info!("Analysis complete in {:.2} seconds", elapsed.as_secs_f64()); + log::debug!("Successfully analyzed database and generated SARIF output"); } Err(ghastoolkit::GHASError::SerdeError(e)) => { log::warn!("Failed to parse SARIF: {e:?}"); + log::debug!("SARIF parsing error details: {:?}", e); } Err(e) => { log::error!("Failed to analyze database: {e:?}"); + log::debug!("Analysis error details: {:?}", e); } }