Skip to content
Merged
93 changes: 82 additions & 11 deletions src/action.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
#![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};
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
Expand Down Expand Up @@ -94,6 +103,13 @@ pub struct Action {
}

impl Action {
/// 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<PathBuf>`: The resolved working directory path
pub fn working_directory(&self) -> Result<PathBuf> {
if self.working_directory.is_empty() {
log::debug!("No working directory provided, using the current directory");
Expand All @@ -108,34 +124,61 @@ 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<Vec<Repository>>`: A list of parsed repository references
pub fn extractor_repository(&self) -> Result<Vec<Repository>> {
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<Repository> = 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::<Vec<Repository>>())
.collect();

log::debug!("Parsed {} repositories", repos.len());
Ok(repos)
}

/// Returns the list of languages to use for CodeQL analysis.
pub fn languages(&self) -> Vec<CodeQLLanguage> {
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
}

/// 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
Expand Down Expand Up @@ -173,6 +216,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<CodeQLLanguage>) -> Result<()> {
for lang in self.languages() {
let mut supported = false;
Expand All @@ -198,6 +246,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");
Expand All @@ -206,6 +257,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 {
Expand Down Expand Up @@ -238,11 +294,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
}
}
Expand All @@ -251,6 +311,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()],
Expand All @@ -259,6 +325,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();
Expand Down
67 changes: 62 additions & 5 deletions src/codeql.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,86 @@
//! 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 ghastoolkit::CodeQL;

/// Download the CodeQL CLI using the GitHub CLI
/// 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<String>` - Path to the installed CodeQL binary or an error
pub async fn gh_codeql_download(codeql_version: &str) -> Result<String> {
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"])
.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])
.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"])
.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");
Expand Down
Loading
Loading