Skip to content
Merged
156 changes: 134 additions & 22 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,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<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 +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<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
}

/// 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<Vec<PathBuf>>`: A vector of possible directory paths for CodeQL operations
///
/// # Errors
/// - If `working_directory()` fails
/// - If path canonicalization fails
fn get_codeql_directories(&self) -> Result<Vec<PathBuf>> {
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
Expand All @@ -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<PathBuf> {
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() {
Expand All @@ -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<CodeQLLanguage>) -> Result<()> {
for lang in self.languages() {
let mut supported = false;
Expand All @@ -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");
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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()],
Expand All @@ -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();
Expand Down
Loading
Loading