From c3147a91c2f12453ca7784e14e25092bb475a9bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:35:16 +0000 Subject: [PATCH 1/3] Initial plan From 239b6a91a9ac6f16f949bedf8f36a80f54975d1f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 22:08:04 +0000 Subject: [PATCH 2/3] Initial plan: patch ghastoolkit dependency to fix compilation errors Co-authored-by: felickz <1760475+felickz@users.noreply.github.com> --- vendor/ghastoolkit/.cargo-ok | 1 + vendor/ghastoolkit/.cargo_vcs_info.json | 6 + vendor/ghastoolkit/.gitignore | 3 + vendor/ghastoolkit/Cargo.toml | 135 +++++ vendor/ghastoolkit/Cargo.toml.orig | 72 +++ vendor/ghastoolkit/README.md | 93 +++ vendor/ghastoolkit/src/codeql/cli.rs | 451 +++++++++++++++ vendor/ghastoolkit/src/codeql/cli/builder.rs | 179 ++++++ vendor/ghastoolkit/src/codeql/cli/models.rs | 8 + vendor/ghastoolkit/src/codeql/database.rs | 545 ++++++++++++++++++ .../ghastoolkit/src/codeql/database/config.rs | 58 ++ .../src/codeql/database/handler.rs | 470 +++++++++++++++ .../src/codeql/database/queries.rs | 237 ++++++++ vendor/ghastoolkit/src/codeql/databases.rs | 304 ++++++++++ vendor/ghastoolkit/src/codeql/download.rs | 146 +++++ vendor/ghastoolkit/src/codeql/languages.rs | 176 ++++++ vendor/ghastoolkit/src/codeql/mod.rs | 92 +++ .../ghastoolkit/src/codeql/packs/handler.rs | 84 +++ vendor/ghastoolkit/src/codeql/packs/loader.rs | 210 +++++++ vendor/ghastoolkit/src/codeql/packs/mod.rs | 11 + vendor/ghastoolkit/src/codeql/packs/models.rs | 81 +++ vendor/ghastoolkit/src/codeql/packs/pack.rs | 245 ++++++++ vendor/ghastoolkit/src/codeql/packs/packs.rs | 79 +++ vendor/ghastoolkit/src/codeql/version.rs | 52 ++ vendor/ghastoolkit/src/codescanning/api.rs | 299 ++++++++++ .../src/codescanning/configuration.rs | 204 +++++++ vendor/ghastoolkit/src/codescanning/mod.rs | 20 + vendor/ghastoolkit/src/codescanning/models.rs | 173 ++++++ vendor/ghastoolkit/src/errors.rs | 82 +++ vendor/ghastoolkit/src/lib.rs | 49 ++ vendor/ghastoolkit/src/octokit/github.rs | 430 ++++++++++++++ vendor/ghastoolkit/src/octokit/mod.rs | 8 + vendor/ghastoolkit/src/octokit/models.rs | 28 + vendor/ghastoolkit/src/octokit/repository.rs | 307 ++++++++++ vendor/ghastoolkit/src/secretscanning/api.rs | 128 ++++ vendor/ghastoolkit/src/secretscanning/mod.rs | 30 + .../src/secretscanning/secretalerts.rs | 130 +++++ .../src/supplychain/dependencies.rs | 147 +++++ .../ghastoolkit/src/supplychain/dependency.rs | 113 ++++ vendor/ghastoolkit/src/supplychain/license.rs | 107 ++++ .../ghastoolkit/src/supplychain/licenses.rs | 155 +++++ vendor/ghastoolkit/src/supplychain/mod.rs | 17 + vendor/ghastoolkit/src/utils/mod.rs | 6 + vendor/ghastoolkit/src/utils/sarif.rs | 214 +++++++ 44 files changed, 6385 insertions(+) create mode 100644 vendor/ghastoolkit/.cargo-ok create mode 100644 vendor/ghastoolkit/.cargo_vcs_info.json create mode 100644 vendor/ghastoolkit/.gitignore create mode 100644 vendor/ghastoolkit/Cargo.toml create mode 100644 vendor/ghastoolkit/Cargo.toml.orig create mode 100644 vendor/ghastoolkit/README.md create mode 100644 vendor/ghastoolkit/src/codeql/cli.rs create mode 100644 vendor/ghastoolkit/src/codeql/cli/builder.rs create mode 100644 vendor/ghastoolkit/src/codeql/cli/models.rs create mode 100644 vendor/ghastoolkit/src/codeql/database.rs create mode 100644 vendor/ghastoolkit/src/codeql/database/config.rs create mode 100644 vendor/ghastoolkit/src/codeql/database/handler.rs create mode 100644 vendor/ghastoolkit/src/codeql/database/queries.rs create mode 100644 vendor/ghastoolkit/src/codeql/databases.rs create mode 100644 vendor/ghastoolkit/src/codeql/download.rs create mode 100644 vendor/ghastoolkit/src/codeql/languages.rs create mode 100644 vendor/ghastoolkit/src/codeql/mod.rs create mode 100644 vendor/ghastoolkit/src/codeql/packs/handler.rs create mode 100644 vendor/ghastoolkit/src/codeql/packs/loader.rs create mode 100644 vendor/ghastoolkit/src/codeql/packs/mod.rs create mode 100644 vendor/ghastoolkit/src/codeql/packs/models.rs create mode 100644 vendor/ghastoolkit/src/codeql/packs/pack.rs create mode 100644 vendor/ghastoolkit/src/codeql/packs/packs.rs create mode 100644 vendor/ghastoolkit/src/codeql/version.rs create mode 100644 vendor/ghastoolkit/src/codescanning/api.rs create mode 100644 vendor/ghastoolkit/src/codescanning/configuration.rs create mode 100644 vendor/ghastoolkit/src/codescanning/mod.rs create mode 100644 vendor/ghastoolkit/src/codescanning/models.rs create mode 100644 vendor/ghastoolkit/src/errors.rs create mode 100644 vendor/ghastoolkit/src/lib.rs create mode 100644 vendor/ghastoolkit/src/octokit/github.rs create mode 100644 vendor/ghastoolkit/src/octokit/mod.rs create mode 100644 vendor/ghastoolkit/src/octokit/models.rs create mode 100644 vendor/ghastoolkit/src/octokit/repository.rs create mode 100644 vendor/ghastoolkit/src/secretscanning/api.rs create mode 100644 vendor/ghastoolkit/src/secretscanning/mod.rs create mode 100644 vendor/ghastoolkit/src/secretscanning/secretalerts.rs create mode 100644 vendor/ghastoolkit/src/supplychain/dependencies.rs create mode 100644 vendor/ghastoolkit/src/supplychain/dependency.rs create mode 100644 vendor/ghastoolkit/src/supplychain/license.rs create mode 100644 vendor/ghastoolkit/src/supplychain/licenses.rs create mode 100644 vendor/ghastoolkit/src/supplychain/mod.rs create mode 100644 vendor/ghastoolkit/src/utils/mod.rs create mode 100644 vendor/ghastoolkit/src/utils/sarif.rs diff --git a/vendor/ghastoolkit/.cargo-ok b/vendor/ghastoolkit/.cargo-ok new file mode 100644 index 0000000..5f8b795 --- /dev/null +++ b/vendor/ghastoolkit/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/vendor/ghastoolkit/.cargo_vcs_info.json b/vendor/ghastoolkit/.cargo_vcs_info.json new file mode 100644 index 0000000..f1e32ab --- /dev/null +++ b/vendor/ghastoolkit/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "0348552de89d2f10b536cdb0248aeb2f82dfe83d" + }, + "path_in_vcs": "core" +} \ No newline at end of file diff --git a/vendor/ghastoolkit/.gitignore b/vendor/ghastoolkit/.gitignore new file mode 100644 index 0000000..13be13f --- /dev/null +++ b/vendor/ghastoolkit/.gitignore @@ -0,0 +1,3 @@ +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock diff --git a/vendor/ghastoolkit/Cargo.toml b/vendor/ghastoolkit/Cargo.toml new file mode 100644 index 0000000..29b206e --- /dev/null +++ b/vendor/ghastoolkit/Cargo.toml @@ -0,0 +1,135 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +rust-version = "1.85" +name = "ghastoolkit" +version = "0.12.2" +authors = ["GeekMasher"] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "GitHub Advanced Security Toolkit in Rust" +documentation = "https://docs.rs/ghastoolkit/latest/ghastoolkit/" +readme = "README.md" +keywords = [ + "github", + "security", + "ghas", +] +categories = ["development-tools"] +license = "MIT" +repository = "https://github.com/GeekMasher/ghastoolkit-rs" +resolver = "2" + +[features] +async = [ + "dep:async-trait", + "dep:tokio", +] +default = ["async"] +toolcache = [ + "async", + "dep:ghactions", +] + +[lib] +name = "ghastoolkit" +path = "src/lib.rs" + +[dependencies.anyhow] +version = "1" + +[dependencies.async-trait] +version = "0.1" +optional = true + +[dependencies.chrono] +version = "0.4" +features = ["serde"] + +[dependencies.ghactions] +version = "^0.18" +features = ["toolcache-all"] +optional = true + +[dependencies.git2] +version = "0.20" + +[dependencies.glob] +version = "0.3" + +[dependencies.http] +version = "1.3" + +[dependencies.log] +version = "0.4" + +[dependencies.octocrab] +version = "^0.47" + +[dependencies.purl] +version = "0.1" +features = ["serde"] + +[dependencies.regex] +version = "1.11" + +[dependencies.reqwest] +version = "^0.12" + +[dependencies.serde] +version = "1" +features = ["derive"] + +[dependencies.serde_json] +version = "1" + +[dependencies.serde_yaml] +version = "0.9" + +[dependencies.thiserror] +version = "2" + +[dependencies.time] +version = "0.3.36" + +[dependencies.tokio] +version = "^1.45" +features = ["full"] +optional = true + +[dependencies.url] +version = "2.5" +features = ["serde"] + +[dependencies.walkdir] +version = "2.5" + +[dependencies.zip] +version = "^6.0" + +[dev-dependencies.anyhow] +version = "1" + +[dev-dependencies.env_logger] +version = "^0.11" + +[dev-dependencies.log] +version = "^0.4" + +[dev-dependencies.tokio] +version = "1.42" +features = ["full"] diff --git a/vendor/ghastoolkit/Cargo.toml.orig b/vendor/ghastoolkit/Cargo.toml.orig new file mode 100644 index 0000000..4c08f7c --- /dev/null +++ b/vendor/ghastoolkit/Cargo.toml.orig @@ -0,0 +1,72 @@ +[package] +name = "ghastoolkit" + +description.workspace = true +version.workspace = true +documentation.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +categories.workspace = true +keywords.workspace = true +authors.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["async"] + +async = ["dep:async-trait", "dep:tokio"] +toolcache = ["async", "dep:ghactions"] + +[dependencies] +anyhow = "1" +thiserror = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_yaml = "0.9" +log = "0.4" +chrono = { version = "0.4", features = ["serde"] } +git2 = "0.20" +glob = "0.3" + +# GitHub API +octocrab = { version = "^0.47" } +reqwest = { version = "^0.12" } +http = "1.3" + +purl = { version = "0.1", features = ["serde"] } +regex = "1.11" +url = { version = "2.5", features = ["serde"] } +walkdir = "2.5" +time = "0.3.36" +zip = "^6.0" + +# For CodeQL in ToolCache +ghactions = { version = "^0.18", features = ["toolcache-all"], optional = true } + +# Async +async-trait = { version = "0.1", optional = true } +tokio = { version = "^1.45", features = ["full"], optional = true} + +[dev-dependencies] +anyhow = "1" +log = "^0.4" +env_logger = "^0.11" +tokio = { version = "1.42", features = ["full"] } + +[[example]] +name = "codeql" +path = "../examples/codeql.rs" +required-features = ["toolcache"] + +[[example]] +name = "codeql-packs" +path = "../examples/codeql-packs/src/main.rs" + +[[example]] +name = "codeql-databases" +path = "../examples/codeql_databases.rs" +required-features = [] diff --git a/vendor/ghastoolkit/README.md b/vendor/ghastoolkit/README.md new file mode 100644 index 0000000..12d5b7d --- /dev/null +++ b/vendor/ghastoolkit/README.md @@ -0,0 +1,93 @@ +# GHASToolkit + +This is the GitHub Advanced Security (GHAS) Toolkit in Rust. +This toolkit is designed to help developers and security researchers to interact with the GitHub Advanced Security API. + +## ✨ Features + +- [Core GHAS Library][code-core] + - [Documentation][docs] + - GitHub Cloud and Enterprise Server support + - API Support + - [x] [Code Scanning][github-code-scanning] + - [x] 👷 [Secret Scanning][github-secret-scanning] + - [x] 👷 [Supply Chain][github-supplychain] + - [ ] 👷 [Dependabot][github-dependabot] (Security Alerts) + - [ ] 👷 [Dependency Graph][github-depgraph] (SCA / SBOMs) + - [ ] 👷 [Security Advisories][github-advisories] +- [CLI Tool][code-cli] + +## 🚀 Usage + +### GitHub APIs + +You can use the `GitHub` and `Repository` structs to interact with the GitHub API. + +```rust no_run +use ghastoolkit::prelude::*; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let github = GitHub::default(); + println!("GitHub :: {}", github); + + let repository = Repository::parse("geekmasher/ghastoolkit-rs@main") + .expect("Failed to parse repository"); + println!("Repository :: {}", repository); + + Ok(()) +} +``` + +### CodeQL + +You can use the `CodeQL` struct to interact with the CodeQL CLI. + +```rust no_run +use ghastoolkit::prelude::*; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let codeql = CodeQL::new().await; + println!("CodeQL :: {}", codeql); + + let languages = codeql.get_languages().await?; + println!("Languages :: {:#?}", languages); + + // Get all CodeQL databases from the default path + let databases = CodeQLDatabases::default(); + for database in databases { + println!("Database :: {}", database); + } + + // Create a new CodeQL database + let database = CodeQLDatabase::init() + .name("my-project") + .language("javascript") + .path("/path/to/code".to_string()) + .build() + .expect("Failed to create CodeQL database"); + + // Create the database using the CodeQL CLI + codeql.database(&database) + .create() + .await?; + + // Run a CodeQL query + codeql.database(&database) + .analyze() + .await?; + + + // You can also download a CodeQL Database from GitHub + let github = GitHub::default(); + let repo = Repository::parse("geekmasher/ghastoolkit-rs@main") + .expect("Failed to parse repository"); + + let databases = CodeQLDatabase::download("./".into(), &repo, &github).await?; + println!("Databases :: {:#?}", databases); + + Ok(()) +} +``` + diff --git a/vendor/ghastoolkit/src/codeql/cli.rs b/vendor/ghastoolkit/src/codeql/cli.rs new file mode 100644 index 0000000..fb795c6 --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/cli.rs @@ -0,0 +1,451 @@ +//! # CodeQL CLI Wrapper + +use log::debug; +use std::{ + fmt::Display, + path::{Path, PathBuf}, +}; +use tokio::io::{AsyncBufReadExt, BufReader}; + +use crate::{ + CodeQLDatabase, CodeQLPack, GHASError, + codeql::{CodeQLLanguage, database::handler::CodeQLDatabaseHandler}, + utils::sarif::Sarif, +}; + +pub mod builder; +mod models; + +use super::{CodeQLExtractor, languages::CodeQLLanguages, packs::handler::CodeQLPackHandler}; +pub use builder::CodeQLBuilder; +use models::ResolvedLanguages; + +/// CodeQL CLI Wrapper to make it easier to run CodeQL commands +#[derive(Debug, Clone)] +pub struct CodeQL { + /// CodeQL CLI Version + version: Option, + /// Path to the CodeQL CLI + path: PathBuf, + /// Number of threads to use + threads: usize, + /// Amount of RAM to use + ram: Option, + /// The search path for the CodeQL CLI + search_path: Vec, + /// Additional packs to use + additional_packs: Vec, + + /// Token to use for authentication with CodeQL registries + token: Option, + + /// Default Suite to use if not specified + pub(crate) suite: Option, + + /// Shows the output of the command + showoutput: bool, +} + +impl CodeQL { + /// Create a new CodeQL instance + #[cfg(not(feature = "async"))] + pub fn new() -> Self { + CodeQL::default() + } + + /// Create a new CodeQL instance + #[cfg(feature = "async")] + pub async fn new() -> Self { + let path = CodeQL::find_codeql().await.unwrap_or_default(); + + CodeQL { + version: CodeQL::get_version(&path).await.ok(), + path, + threads: 0, + ram: None, + search_path: Vec::new(), + additional_packs: Vec::new(), + token: None, + suite: None, + showoutput: true, + } + } + + /// Get the CodeQL CLI path + pub fn path(&self) -> &PathBuf { + &self.path + } + + /// Set the CodeQL CLI path + pub(crate) fn set_path(&mut self, path: PathBuf) { + log::trace!("Setting CodeQL path to {:?}", path); + self.path = path; + } + + /// Initialize a new CodeQL Builder instance + pub fn init() -> CodeQLBuilder { + CodeQLBuilder::default() + } + + /// Get the search paths set for the CodeQL CLI to use. + /// + /// Paths are separated by a colon + pub(crate) fn search_paths(&self) -> String { + self.search_path + .iter() + .map(|p| p.to_str().unwrap().to_string()) + .collect::>() + .join(":") + } + + /// Add the search path to the CodeQL CLI arguments + pub(crate) fn add_search_path(&self, args: &mut Vec) { + if !self.search_path.is_empty() { + args.push("--search-path".to_string()); + args.push(self.search_paths()); + } + } + + /// Append a search path to the CodeQL CLI + pub fn append_search_path(&mut self, path: impl Into) { + self.search_path.push(path.into()); + } + + /// Add the additional packs to the CodeQL CLI arguments + pub(crate) fn add_additional_packs(&self, args: &mut Vec) { + if !self.additional_packs.is_empty() { + args.push("--additional-packs".to_string()); + args.push(self.additional_packs.join(",")); + } + } + + /// Get the default suite for the CodeQL CLI + pub fn default_suite(&self) -> String { + self.suite.clone().unwrap_or("code-scanning".to_string()) + } + /// Add an external extractor to the CodeQL CLI (search path) + pub fn add_extractor(&mut self, extractor: &CodeQLExtractor) { + self.search_path.push(extractor.path.clone()); + } + + /// Find CodeQL CLI on the system (asynchronous) + pub async fn find_codeql() -> Option { + // Root CodeQL Paths + if let Some(e) = std::env::var_os("CODEQL_PATH") { + let p = PathBuf::from(e).join("codeql"); + if p.exists() && p.is_file() { + return Some(p); + } + } else if let Some(e) = std::env::var_os("CODEQL_BINARY") { + let p = PathBuf::from(e); + if p.exists() && p.is_file() { + return Some(p); + } + } + #[cfg(feature = "toolcache")] + { + if let Some(t) = CodeQL::find_codeql_toolcache().await { + log::debug!("Found CodeQL in toolcache: {:?}", t); + return Some(t); + } + } + if let Some(p) = CodeQL::find_codeql_path() { + log::debug!("Found CodeQL in PATH: {:?}", p); + return Some(p); + } + + None + } + + /// Load a CodeQL extractor from a path + pub async fn load_extractor( + &mut self, + path: impl Into, + ) -> Result { + let path = path.into(); + + let extractor = CodeQLExtractor::load_path(&path)?; + self.search_path.push(path); + + Ok(extractor) + } + + fn find_codeql_path() -> Option { + debug!("Looking for CodeQL in PATH"); + // Check if CodeQL is in the PATH + if let Ok(paths) = std::env::var("PATH") { + for path in paths.split(':') { + let p = Path::new(path).join("codeql"); + if p.exists() && p.is_file() { + return Some(p); + } + } + } + None + } + + #[cfg(feature = "toolcache")] + async fn find_codeql_toolcache() -> Option { + let toolcache = ghactions::ToolCache::new(); + if let Ok(tool) = toolcache.find("CodeQL", "latest").await { + let tool = tool.path(); + // TODO: This needs to be better + if tool.join("codeql").is_file() { + return Some(tool.join("codeql")); + } else if tool.join("codeql").is_dir() && tool.join("codeql").join("codeql").is_file() { + return Some(tool.join("codeql").join("codeql")); + } + } + None + } + + /// Run a CodeQL command asynchronously + /// + /// This function will run the CodeQL command with the given arguments and return the output. + /// + /// It will also set the `CODEQL_REGISTRIES_AUTH` environment variable if a token is provided. + pub async fn run(&self, args: Vec>) -> Result { + let args: Vec = args.iter().map(|arg| arg.as_ref().to_string()).collect(); + debug!("CodeQL::run({:?})", args); + + // Insert CODEQL_REGISTRIES_AUTH to the env + let mut envs = std::env::vars_os() + .map(|(k, v)| (k.to_string_lossy().to_string(), v)) + .collect::>(); + if let Some(token) = &self.token { + envs.insert( + "CODEQL_REGISTRIES_AUTH".to_string(), + token.to_string().into() + ); + } + + + let mut cmd = tokio::process::Command::new(&self.path) + .args(args) + .envs(envs) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .spawn()?; + + let stdout = cmd.stdout.take().ok_or(GHASError::CodeQLError( + "Failed to get stdout from CodeQL command".to_string(), + ))?; + + let reader = BufReader::new(stdout); + let mut lines = reader.lines(); + + let mut output_lines = Vec::new(); + + while let Some(line) = lines.next_line().await? { + if self.showoutput { + println!("{}", line); // print live + } + output_lines.push(line); // store for later + } + + let status = cmd.wait().await?; + + if status.success() { + debug!("CodeQL Command Success: {:?}", status.to_string()); + Ok(output_lines.join("\n")) + } else { + Err(GHASError::CodeQLError( + "Error running CodeQL command".to_string(), + )) + } + } + + /// Run a CodeQL command without showing the output + /// + /// This is an internal function and should not be used directly. + pub(crate) async fn rn(&self, args: Vec>) -> Result { + let mut codeql = self.clone(); + codeql.showoutput = false; + codeql.run(args).await + } + + /// Pass a CodeQLDatabase to the CodeQL CLI to return a CodeQLDatabaseHandler. + /// This handler can be used to run queries and other operations on the database. + #[allow(elided_named_lifetimes)] + pub fn database<'a>(&'a self, db: &'a CodeQLDatabase) -> CodeQLDatabaseHandler { + CodeQLDatabaseHandler::new(db, self) + } + + /// Pass a CodeQLPack to the CodeQL CLI to return a CodeQLPackHandler. + /// + /// This handler can be used to run queries and other operations on the pack. + #[allow(elided_named_lifetimes)] + pub fn pack<'a>(&'a self, pack: &'a CodeQLPack) -> CodeQLPackHandler { + CodeQLPackHandler::new(pack, self) + } + + /// An async function to run a CodeQL scan on a database. + /// + /// This includes the following steps: + /// - Creating the database + /// - Running the analysis + /// + /// # Example + /// + /// ```no_run + /// use ghastoolkit::codeql::{CodeQL, CodeQLDatabase}; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let codeql = CodeQL::new().await; + /// + /// let mut db = CodeQLDatabase::init() + /// .source("./") + /// .language("python") + /// .build() + /// .expect("Failed to create database"); + /// + /// let sarif = codeql.scan(&mut db, "codeql/python-queries").await + /// .expect("Failed to run scan"); + /// // ... do something with the sarif + /// # } + /// ``` + pub async fn scan<'a>( + &'a self, + db: &'a mut CodeQLDatabase, + queries: impl Into, + ) -> Result { + self.database(db).overwrite().create().await?; + + let sarif = db.path().join("results.sarif"); + self.database(db) + .sarif(sarif.clone()) + .queries(queries.into()) + .analyze() + .await?; + + db.reload()?; + + self.sarif(sarif) + } + + /// Get the SARIF file from the CodeQL CLI + pub fn sarif(&self, path: impl Into) -> Result { + Ok(Sarif::try_from(path.into())?) + } + + /// Get the version of the loaded CodeQL CLI + pub fn version(&self) -> Option { + self.version.clone() + } + + /// Check to see if the CodeQL CLI is installed + pub async fn is_installed(&self) -> bool { + Self::get_version(&self.path).await.is_ok() + } + + /// Get the version of the CodeQL CLI + pub async fn get_version(path: &Path) -> Result { + log::debug!("CodeQL.get_version path :: {:?}", path); + let output = tokio::process::Command::new(path) + .args(["version", "--format", "terse"]) + .output() + .await?; + + if output.status.success() { + debug!("CodeQL Command Success: {:?}", output.status.to_string()); + Ok(String::from_utf8_lossy(&output.stdout) + .to_string() + .trim() + .to_string()) + } else { + Err(GHASError::CodeQLError( + String::from_utf8_lossy(&output.stderr).to_string(), + )) + } + } + + /// Get the programming languages supported by the CodeQL CLI. + /// This function will return the primary languages supported by the CodeQL and exclude + /// any secondary languages (checkout `get_secondary_languages()`). + /// + /// # Example + /// + /// ```no_run + /// use ghastoolkit::CodeQL; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let codeql = CodeQL::default(); + /// + /// let languages = codeql.get_languages() + /// .await + /// .expect("Failed to get languages"); + /// + /// for language in languages { + /// println!("Language: {}", language.pretty()); + /// // Do something with the language + /// } + /// + /// # } + /// ``` + pub async fn get_languages(&self) -> Result, GHASError> { + Ok(self.get_all_languages().await?.get_languages()) + } + + /// Get the secondary languages supported by the CodeQL CLI + pub async fn get_secondary_languages(&self) -> Result, GHASError> { + Ok(self.get_all_languages().await?.get_secondary()) + } + + /// Get all languages supported by the CodeQL CLI + pub async fn get_all_languages(&self) -> Result { + let mut args = vec!["resolve", "languages", "--format", "json"]; + + let search_path = self.search_paths(); + if !self.search_path.is_empty() { + args.push("--search-path"); + args.push(&search_path); + } + + log::debug!("CodeQL.get_all_languages args :: {:?}", args); + + match self.rn(args).await { + Ok(v) => { + let languages: ResolvedLanguages = serde_json::from_str(&v)?; + let mut result = Vec::new(); + for (language, path) in languages { + // allow custom languages if they come from CodeQL CLI + result.push(CodeQLLanguage::from(( + language, + PathBuf::from(path.first().unwrap()), + ))); + } + result.sort(); + Ok(CodeQLLanguages::new(result)) + } + Err(e) => Err(e), + } + } +} + +impl Display for CodeQL { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(version) = &self.version { + write!(f, "CodeQL('{}', '{}')", self.path.display(), version) + } else { + write!(f, "CodeQL('{}')", self.path.display()) + } + } +} + +impl Default for CodeQL { + fn default() -> Self { + CodeQL { + version: None, + path: PathBuf::new(), + threads: 0, + ram: None, + search_path: Vec::new(), + additional_packs: Vec::new(), + token: None, + suite: None, + showoutput: true, + } + } +} diff --git a/vendor/ghastoolkit/src/codeql/cli/builder.rs b/vendor/ghastoolkit/src/codeql/cli/builder.rs new file mode 100644 index 0000000..f55bfa4 --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/cli/builder.rs @@ -0,0 +1,179 @@ +//! # CodeQL Builder +//! +//! This module provides a builder for the CodeQL CLI. + +use std::path::PathBuf; + +use crate::GHASError; + +use super::CodeQL; + +/// CodeQL Builder to make it easier to create a new CodeQL instance +#[derive(Debug, Clone, Default)] +pub struct CodeQLBuilder { + path: Option, + + threads: usize, + ram: usize, + + search_paths: Vec, + additional_packs: Vec, + + token: Option, + + suite: Option, + showoutput: bool, +} + +impl CodeQLBuilder { + /// Set the path to the CodeQL CLI + /// + /// ```rust + /// use ghastoolkit::CodeQL; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let codeql = CodeQL::init() + /// .path("/path/to/codeql") + /// .build() + /// .await + /// .expect("Failed to create CodeQL instance"); + /// # } + /// ``` + pub fn path(mut self, path: impl Into) -> Self { + let path = path.into(); + self.path = Some(path); + self + } + + /// Set manually the threads for CodeQL + pub fn threads(mut self, threads: usize) -> Self { + self.threads = threads; + self + } + + /// Set manually the ram for CodeQL + pub fn ram(mut self, ram: usize) -> Self { + self.ram = ram; + self + } + + /// Add additional packs to the CodeQL CLI + pub fn additional_packs(mut self, path: String) -> Self { + self.additional_packs.push(path); + self + } + + /// Add a search path to the CodeQL CLI + /// + /// ```rust + /// use ghastoolkit::codeql::cli::CodeQL; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let codeql = CodeQL::init() + /// .search_path("/path/to/codeql") + /// .build() + /// .await + /// .expect("Failed to create CodeQL instance"); + /// # } + /// ``` + pub fn search_path(mut self, path: impl Into) -> Self { + self.search_paths.push(path.into()); + self + } + + /// Set the default suite for the CodeQL CLI to use + pub fn suite(mut self, suite: impl Into) -> Self { + self.suite = Some(suite.into()); + self + } + + /// Set the show output flag for the CodeQL CLI + pub fn show_output(mut self, show: bool) -> Self { + self.showoutput = show; + self + } + + /// Set the token for the CodeQL CLI + /// + /// ```no_run + /// use ghastoolkit::CodeQL; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let codeql = CodeQL::init() + /// .token("my-token") + /// .build() + /// .await + /// .expect("Failed to create CodeQL instance"); + /// # } + /// ``` + pub fn token(mut self, token: impl Into) -> Self { + self.token = Some(token.into()); + self + } + + /// Build the CodeQL instance + pub async fn build(&self) -> Result { + let path: PathBuf = match self.path { + Some(ref p) => p.clone(), + None => match CodeQL::find_codeql().await { + Some(p) => p, + None => PathBuf::new(), + }, + }; + log::debug!("CodeQL CLI path: {:?}", path); + + let version: Option = CodeQL::get_version(&path).await.ok(); + + Ok(CodeQL { + version, + path, + threads: self.threads, + ram: self.ram.into(), + additional_packs: self.additional_packs.clone(), + search_path: self.search_paths.clone(), + token: self.token.clone(), + suite: self.suite.clone(), + showoutput: self.showoutput, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_codeql_builder() { + let codeql = CodeQLBuilder::default() + .path("/path/to/codeql") + .threads(4) + .ram(8) + .additional_packs("my-pack".to_string()) + .search_path("/path/to/search") + .show_output(true) + .build() + .await + .unwrap(); + + assert_eq!(codeql.path, PathBuf::from("/path/to/codeql")); + assert_eq!(codeql.threads, 4); + assert_eq!(codeql.ram, Some(8)); + assert_eq!(codeql.additional_packs, vec!["my-pack".to_string()]); + assert_eq!(codeql.search_path, vec![PathBuf::from("/path/to/search")]); + assert_eq!(codeql.showoutput, true); + } + + #[tokio::test] + async fn test_codeql_builder_token() { + let codeql = CodeQLBuilder::default() + .token("my-token") + .build() + .await + .unwrap(); + + assert_eq!(codeql.token, Some("my-token".to_string())); + } +} diff --git a/vendor/ghastoolkit/src/codeql/cli/models.rs b/vendor/ghastoolkit/src/codeql/cli/models.rs new file mode 100644 index 0000000..f902877 --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/cli/models.rs @@ -0,0 +1,8 @@ +use std::collections::HashMap; + +/// JSON representation of the languages supported by the CodeQL CLI +/// +/// ```bash +/// codeql resolve languages --format json +/// ``` +pub(crate) type ResolvedLanguages = HashMap>; diff --git a/vendor/ghastoolkit/src/codeql/database.rs b/vendor/ghastoolkit/src/codeql/database.rs new file mode 100644 index 0000000..aa95d5c --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/database.rs @@ -0,0 +1,545 @@ +//! # CodeQL Database +//! +//! This module defines the CodeQL Database. +//! This structure is used to interact with CodeQL databases. +//! It provides methods to validate, build, and handle databases. + +use std::{ + fmt::Display, + path::{Path, PathBuf}, +}; + +use log::debug; + +use crate::{ + CodeQLDatabases, GHASError, GitHub, Repository, + codeql::{CodeQLLanguage, database::config::CodeQLDatabaseConfig}, +}; + +pub mod config; +pub mod handler; +pub mod queries; + +/// CodeQL Database +#[derive(Debug, Clone, Default)] +pub struct CodeQLDatabase { + name: String, + /// The path to the database + path: PathBuf, + /// The language of the database + language: CodeQLLanguage, + /// The source root of the database + source: Option, + /// Repository the database is associated with + repository: Option, + /// Configuration + config: Option, +} + +impl CodeQLDatabase { + /// Create a new CodeQLDatabase + pub fn new() -> Self { + Self::default() + } + + /// Initialize a new CodeQLDatabaseBuilder + pub fn init() -> CodeQLDatabaseBuilder { + CodeQLDatabaseBuilder::default() + } + + /// Get the database name + pub fn name(&self) -> String { + if self.name.is_empty() { + // Repo + if let Some(ref repo) = self.repository { + return repo.name().to_string(); + } else if self.path.exists() { + // TODO: This only works for the default path + let base = CodeQLDatabases::default_path(); + let path = self.path.strip_prefix(&base).unwrap_or(&self.path); + let components = path.components().collect::>(); + if components.len() == 1 { + // If there is only one component, it is the name + return components[0].as_os_str().to_string_lossy().to_string(); + } else if components.len() > 1 { + // If there are more than one component, it is the name + return components[1].as_os_str().to_string_lossy().to_string(); + } + } else if let Some(source) = &self.source { + // TODO(geekmasher): This is a bit of a hack, but it works for now + return source + .clone() + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_string(); + } + } + self.name.clone() + } + + /// Get the database language + pub fn language(&self) -> &str { + self.language.language() + } + + /// Get the repository the database is associated with + pub fn repository(&self) -> Option<&Repository> { + self.repository.as_ref() + } + + /// Set the repository the database is associated with + pub(crate) fn set_repository(&mut self, repository: &Repository) { + self.repository = Some(repository.clone()); + self.name = repository.name().to_string(); + } + + /// Get the database path (root directory) + pub fn path(&self) -> &PathBuf { + &self.path + } + + /// Get the path to the CodeQL Database configuration file + pub fn configuration_path(&self) -> PathBuf { + let mut path = self.path.clone(); + path.push("codeql-database.yml"); + path + } + + /// Creation time of the database + pub fn created_at(&self) -> Option> { + if let Some(config) = &self.config { + if let Some(metadata) = &config.creation_metadata { + return Some(metadata.creation_time); + } + } + None + } + + /// Check if the database is valid (configuration file exists) + pub fn validate(&self) -> bool { + let path = self.configuration_path(); + path.exists() + } + + /// Get the version of the CodeQL CLI used to create the database + /// If the version is not available, it will return "0.0.0" + pub fn version(&self) -> String { + if let Some(config) = &self.config { + if let Some(metadata) = &config.creation_metadata { + return metadata.cli_version.clone(); + } + } + String::from("0.0.0") + } + + /// Get the creation time of the database + pub fn creation_time(&self) -> Option> { + if let Some(config) = &self.config { + if let Some(metadata) = &config.creation_metadata { + return Some(metadata.creation_time); + } + } + None + } + + /// Get the number of lines of code in the database + pub fn lines_of_code(&self) -> usize { + if let Some(config) = &self.config { + return config.baseline_lines_of_code; + } + 0 + } + + /// Reload the database configuration + pub fn reload(&mut self) -> Result<(), GHASError> { + debug!("Reloading CodeQL Database Configuration"); + if self.validate() { + let config = CodeQLDatabaseConfig::read(&self.configuration_path())?; + self.config = Some(config); + Ok(()) + } else { + Err(GHASError::CodeQLDatabaseError( + "Invalid CodeQL Database".to_string(), + )) + } + } + + /// Load a database from a directory + pub fn load(path: impl Into) -> Result { + let path = path.into(); + if !path.exists() { + return Err(GHASError::CodeQLDatabaseError( + "Could not find codeql-database.yml".to_string(), + )); + } + + // If the path is a file, we need to pop it to get the directory + if path.is_file() && path.ends_with("codeql-database.yml") { + debug!("Loading CodeQL Database from: {}", path.display()); + CodeQLDatabase::load_database_config(&path) + } else { + log::debug!("Finding CodeQL Database from: {}", path.display()); + // If the path is a directory, we need to find the configuration file + let mut dbroot = PathBuf::new(); + + // Look for `codeql-database.yml` in the directory recursively + for entry in walkdir::WalkDir::new(&path) { + let entry = entry?; + if entry.file_name() == "codeql-database.yml" { + dbroot = entry.path().to_path_buf(); + break; + } + } + + log::debug!("Loading CodeQL Database from: {}", dbroot.display()); + CodeQLDatabase::load_database_config(&dbroot) + } + } + + /// Download / Fetch database from GitHub + pub async fn download( + output: PathBuf, + repository: &Repository, + github: &GitHub, + ) -> Result, GHASError> { + let mut databases = CodeQLDatabases::new(); + databases.set_path(output); + databases.download(repository, github).await?; + Ok(databases.databases()) + } + + fn load_database_config(path: &PathBuf) -> Result { + if !path.exists() { + Err(GHASError::CodeQLDatabaseError( + "Could not find codeql-database.yml".to_string(), + )) + } else { + match CodeQLDatabaseConfig::read(path) { + Ok(config) => CodeQLDatabase::init() + .path(path.parent().unwrap_or(path)) + .source(config.source_location_prefix.clone().unwrap_or_default()) + .language(config.primary_language.clone()) + .config(config.clone()) + .build(), + Err(err) => { + log::error!("Failed to load database configuration: {}", err); + Err(GHASError::CodeQLDatabaseError( + "Failed to load database configuration".to_string(), + )) + } + } + } + } +} + +impl From for CodeQLDatabase { + fn from(path: String) -> Self { + CodeQLDatabase::load(path).expect("Failed to load CodeQL Database") + } +} + +impl From<&str> for CodeQLDatabase { + fn from(path: &str) -> Self { + CodeQLDatabase::load(path.to_string()).expect("Failed to load CodeQL Database") + } +} + +impl From for CodeQLDatabase { + fn from(path: PathBuf) -> Self { + CodeQLDatabase::load(path.to_string_lossy().to_string()) + .expect("Failed to load CodeQL Database") + } +} + +impl From<&Path> for CodeQLDatabase { + fn from(path: &Path) -> Self { + CodeQLDatabase::load(path.to_string_lossy().to_string()) + .expect("Failed to load CodeQL Database") + } +} + +impl Display for CodeQLDatabase { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let version = self.version(); + if version.as_str() == "0.0.0" { + write!(f, "CodeQLDatabase('{}', '{}')", self.name(), self.language) + } else { + write!( + f, + "CodeQLDatabase('{}', '{}', '{}')", + self.name(), + self.language, + version + ) + } + } +} + +/// CodeQL Database Builder used for creating a new CodeQLDatabase's using the builder pattern +/// +/// # Example +/// +/// ```rust +/// use ghastoolkit::codeql::CodeQLDatabase; +/// +/// // Using the `init` method to create a new CodeQLDatabaseBuilder +/// let database = CodeQLDatabase::init() +/// .name("test".to_string()) +/// .path("/path/to/database".to_string()) +/// .language("javascript".to_string()) +/// .source("/path/to/source".to_string()) +/// .build() +/// .expect("Failed to build database"); +/// +/// ``` +#[derive(Debug, Clone, Default)] +pub struct CodeQLDatabaseBuilder { + name: String, + path: Option, + language: CodeQLLanguage, + source: Option, + repository: Option, + config: Option, +} + +impl CodeQLDatabaseBuilder { + /// Set the name of the database + /// + /// **Example:** + /// + /// ```rust + /// use ghastoolkit::CodeQLDatabase; + /// + /// let database = CodeQLDatabase::init() + /// .name("test") + /// .path("/path/to/database") + /// .language("javascript") + /// .build() + /// .unwrap(); + /// ``` + pub fn name(mut self, name: impl Into) -> Self { + self.name = name.into(); + self + } + + /// Set the path to the database. If there is an existing database, it will load the configuration + /// file from the path. + /// + /// **Example:** + /// + /// ```rust + /// use ghastoolkit::CodeQLDatabase; + /// + /// let database = CodeQLDatabase::init() + /// .name("test") + /// .path("/path/to/database") + /// .build() + /// .unwrap(); + /// ``` + pub fn path(mut self, path: impl Into) -> Self { + let path = path.into(); + self.path = Some(path.clone()); + + if path.exists() { + let config_path = if path.is_dir() { + path.join("codeql-database.yml") + } else if path.is_file() { + path.clone() + } else { + log::warn!("Unknown path type: {:?}", path); + return self; + }; + + debug!("Loading database configuration: {:?}", &config_path); + + let config = match CodeQLDatabaseConfig::read(&config_path) { + Ok(config) => config, + Err(e) => { + debug!("Failed to load database configuration: {}", e); + return self; + } + }; + debug!("Loaded database configuration: {:?}", &path); + + self.language = CodeQLLanguage::from(config.primary_language); + if let Some(source) = config.source_location_prefix { + self.source = Some(PathBuf::from(source)); + } + } + self + } + + /// Set the source root for database creation / mapping + /// + /// **Example:** + /// + /// ```rust + /// use ghastoolkit::CodeQLDatabase; + /// + /// let database = CodeQLDatabase::init() + /// .name("test") + /// .source("./src") + /// .build() + /// .unwrap(); + /// ``` + pub fn source(mut self, source: impl Into) -> Self { + let source = source.into(); + self.source = Some(PathBuf::from(source)); + self + } + + /// Set the language of the database + /// + /// **Examples:** + /// + /// ```rust + /// use ghastoolkit::CodeQLDatabase; + /// + /// let database = CodeQLDatabase::init() + /// .name("test") + /// .language("javascript") + /// .build() + /// .unwrap(); + /// ``` + pub fn language(mut self, language: impl Into) -> Self { + self.language = language.into(); + self + } + + /// Set the repository the database is associated with + pub fn repository(mut self, repository: &Repository) -> Self { + self.name = repository.name().to_string(); + self.repository = Some(repository.clone()); + self + } + + /// Set the configuration for the database + pub fn config(mut self, config: CodeQLDatabaseConfig) -> Self { + self.language = CodeQLLanguage::from(config.primary_language.clone()); + self.config = Some(config); + self + } + + /// Get the default path for the database + pub(crate) fn default_path(&self) -> PathBuf { + let mut path = CodeQLDatabases::default_path(); + + if let Some(ref repo) = self.repository { + path.push(repo.owner()); + path.push(repo.name()); + path.push(self.language.language()); + } else if !self.language.is_secondary() { + path.push(format!("{}-{}", self.language.language(), self.name)); + } else { + path.push(self.name.clone()); + } + + path + } + + /// Build the CodeQLDatabase from the builder + pub fn build(&self) -> Result { + let path = match self.path.clone() { + Some(p) => p, + None => self.default_path(), + }; + + let name = if self.name.is_empty() { + if let Some(ref repo) = self.repository { + // If a repository is set, use its name + repo.name().to_string() + } else if let Some(ref source) = self.source { + // If the source is set, use it to derive the name + source + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("unknown") + .to_string() + } else { + // Fallback to the path if no name is set + path.file_name() + .and_then(|s| s.to_str()) + .unwrap_or("unknown") + .to_string() + } + } else { + self.name.clone() + }; + + Ok(CodeQLDatabase { + name, + path, + language: self.language.clone(), + source: self.source.clone(), + repository: self.repository.clone(), + config: self.config.clone(), + }) + } +} + +#[cfg(test)] +mod tests { + use crate::{CodeQLDatabase, Repository}; + use std::path::PathBuf; + + #[test] + fn test_default_database_path() { + let base = match std::env::var("HOME") { + Ok(p) => { + let mut path = PathBuf::from(p); + path.push(".codeql"); + path.push("databases"); + path + } + Err(_) => PathBuf::from("/tmp/codeql"), + }; + + let mut path = base.clone(); + path.push("python-test-repo"); + + let db = CodeQLDatabase::init() + .name(String::from("test-repo")) + .language("python".to_string()) + .build() + .expect("Failed to build database"); + + assert_eq!(db.language(), "python"); + assert_eq!(db.path, path); + } + + #[test] + fn test_database_name() { + // Set the name of the database + let db = CodeQLDatabase::init() + .name(String::from("test-repo")) + .language("python".to_string()) + .build() + .expect("Failed to build database"); + + assert_eq!(db.name, "test-repo"); + } + + #[test] + fn test_database_from_source() { + let db2 = CodeQLDatabase::init() + .source(String::from("/tmp/test-repo")) + .language("python".to_string()) + .build() + .expect("Failed to build database"); + assert_eq!(db2.name, "test-repo"); + } + + #[test] + fn test_database_from_repository() { + let repo = Repository::parse("geekmasher/test-repo").unwrap(); + let db3 = CodeQLDatabase::init() + .repository(&repo) + .language("python".to_string()) + .build() + .expect("Failed to build database"); + + assert_eq!(db3.name, "test-repo"); + } +} diff --git a/vendor/ghastoolkit/src/codeql/database/config.rs b/vendor/ghastoolkit/src/codeql/database/config.rs new file mode 100644 index 0000000..a694d86 --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/database/config.rs @@ -0,0 +1,58 @@ +//! # CodeQL Database Configuration + +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +use crate::GHASError; + +/// CodeQL Database Configuration which is stored in the database directory +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CodeQLDatabaseConfig { + /// Source Location Prefix + #[serde(rename = "sourceLocationPrefix")] + pub source_location_prefix: Option, + /// Database primary language (e.g. cpp, java, python, etc.) + #[serde(rename = "primaryLanguage")] + pub primary_language: String, + /// Database baseline lines of code + #[serde(rename = "baselineLinesOfCode")] + pub baseline_lines_of_code: usize, + /// Unicode Newlines + #[serde(rename = "unicodeNewlines")] + pub unicode_newlines: bool, + /// Database column kind + #[serde(rename = "columnKind")] + pub column_kind: String, + /// Database creation metadata + #[serde(rename = "creationMetadata")] + pub creation_metadata: Option, + /// Build Mode + #[serde(rename = "buildMode")] + pub build_mode: Option, + /// Finalized + #[serde(default, rename = "finalised")] + pub finalised: bool, +} + +impl CodeQLDatabaseConfig { + /// Read, parse, and return a CodeQL Database Configuration from the provided path + pub fn read(path: &PathBuf) -> Result { + let file = std::fs::File::open(path)?; + let reader = std::io::BufReader::new(file); + let config: CodeQLDatabaseConfig = serde_yaml::from_reader(reader)?; + Ok(config) + } +} + +/// CodeQL Database Configuration Metadata +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CodeQLDatabaseConfigMetadata { + /// Database SHA + pub sha: Option, + /// Database CLI Version + #[serde(rename = "cliVersion")] + pub cli_version: String, + /// Database Creation Time + #[serde(rename = "creationTime")] + pub creation_time: chrono::DateTime, +} diff --git a/vendor/ghastoolkit/src/codeql/database/handler.rs b/vendor/ghastoolkit/src/codeql/database/handler.rs new file mode 100644 index 0000000..8563169 --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/database/handler.rs @@ -0,0 +1,470 @@ +//! # CodeQL Database Handler +use std::path::PathBuf; + +use crate::{ + CodeQL, CodeQLDatabase, CodeQLDatabases, GHASError, codeql::database::queries::CodeQLQueries, +}; + +/// CodeQL Database Handler +#[derive(Debug, Clone)] +pub struct CodeQLDatabaseHandler<'db, 'ql> { + /// Reference to the CodeQL Database + database: &'db CodeQLDatabase, + /// Reference to the CodeQL instance + codeql: &'ql CodeQL, + /// Query / Pack / Suites + queries: CodeQLQueries, + /// Threat Models + threat_models: Vec, + /// Model Packs + model_packs: Vec, + /// SARIF Category + category: Option, + /// Build command to create the database (for compiled languages) + command: Option, + /// Output for Analysis + output: PathBuf, + /// Format for Analysis + output_format: String, + /// Overwrite the database if it exists + overwrite: bool, + /// Summary of the analysis + summary: bool, +} + +impl<'db, 'ql> CodeQLDatabaseHandler<'db, 'ql> { + /// Create a new CodeQL Database Handler + pub fn new(database: &'db CodeQLDatabase, codeql: &'ql CodeQL) -> Self { + Self { + database, + codeql, + // Default to standard query packs + queries: CodeQLQueries::language_default(database.language.language()), + threat_models: Vec::new(), + model_packs: Vec::new(), + category: None, + command: None, + output: CodeQLDatabaseHandler::default_results(database), + output_format: String::from("sarif-latest"), + overwrite: false, + summary: true, + } + } + + /// Set the build command to create the database (for compiled languages) + pub fn command(mut self, command: String) -> Self { + self.command = Some(command); + log::trace!("Setting build command: {:?}", self.command); + self + } + + /// Set the output for Analysis + pub fn output(mut self, output: PathBuf) -> Self { + self.output = output.clone(); + log::trace!("Setting output path: {:?}", self.output); + self + } + + /// Set the CodeQL output path and format to `sarif` (JSON) + pub fn sarif(mut self, output: impl Into) -> Self { + self.output = output.into(); + self.output_format = String::from("sarif-latest"); + log::trace!("Setting output format to SARIF"); + self + } + + /// Set the CodeQL output path and format to `csv` + pub fn csv(mut self, output: impl Into) -> Self { + self.output = output.into(); + self.output_format = String::from("csv"); + log::trace!("Setting output format to CSV"); + self + } + + /// Set the queries / packs / suites to use for the analysis + pub fn queries(mut self, queries: impl Into) -> Self { + self.queries = queries.into(); + log::trace!("Setting queries: {:?}", self.queries); + self + } + + /// Set the query pack and suite to use for the analysis + pub fn suite(mut self, queries: impl Into) -> Self { + let queries: String = queries.into(); + + match queries.as_str() { + "security-extended" => { + self.queries = CodeQLQueries::language_default(self.database.language.language()); + self.queries.set_path(format!( + "codeql-suites/{}-security-extended.qls", + self.database.language.language() + )); + } + "security-and-quality" => { + self.queries = CodeQLQueries::language_default(self.database.language.language()); + self.queries.set_path(format!( + "codeql-suites/{}-security-and-quality.qls", + self.database.language.language() + )); + } + "experimental" => { + self.queries = CodeQLQueries::language_default(self.database.language.language()); + self.queries.set_path(format!( + "codeql-suites/{}-experimental.qls", + self.database.language.language() + )); + } + "default" | "code-scanning" => { + self.queries = CodeQLQueries::language_default(self.database.language.language()); + self.queries.set_path(format!( + "codeql-suites/{}-code-scanning.qls", + self.database.language.language() + )); + } + _ => { + self.queries = CodeQLQueries::from(queries.clone()); + } + } + + log::trace!("Setting queries: {:?}", self.queries); + self + } + + /// Set a Threat Model for the analysis + pub fn threat_model(mut self, threat_model: impl Into) -> Self { + self.threat_models.push(threat_model.into()); + log::trace!("Setting threat model: {:?}", self.threat_models); + self + } + + /// Set Threat Models for the analysis + pub fn threat_models(mut self, threat_models: Vec>) -> Self { + self.threat_models = threat_models.into_iter().map(|tm| tm.into()).collect(); + log::trace!("Setting threat models: {:?}", self.threat_models); + self + } + + /// Disable the default threat model + pub fn disable_default_threat_model(mut self) -> Self { + self.threat_models.push("!default".to_string()); + log::trace!("Disabling default threat model"); + self + } + + /// Set Model Packs for the analysis + pub fn model_pack(mut self, model_pack: impl Into) -> Self { + self.model_packs.push(model_pack.into()); + log::trace!("Setting model pack: {:?}", self.model_packs); + self + } + + /// Set Model Packs for the analysis + /// + /// This replaces any existing model packs with the provided ones + pub fn model_packs(mut self, model_packs: Vec>) -> Self { + self.model_packs = model_packs.into_iter().map(|p| p.into()).collect(); + log::trace!("Setting model packs: {:?}", self.model_packs); + self + } + + /// Set the SARIF category for the analysis + pub fn category(mut self, category: impl Into) -> Self { + self.category = Some(category.into()); + log::trace!("Setting SARIF category: {:?}", self.category); + self + } + + /// Overwrite the database if it exists + pub fn overwrite(mut self) -> Self { + self.overwrite = true; + log::trace!("Overwriting database if it exists"); + self + } + + /// Output the summary of the analysis + pub fn summary(mut self, summary: bool) -> Self { + self.summary = summary; + log::trace!("Setting summary: {:?}", self.summary); + self + } + + /// Create a new CodeQL Database using the provided database + pub async fn create(&mut self) -> Result<(), GHASError> { + log::debug!("Creating CodeQL Database: {:?}", self.database); + + let args = self.create_cmd()?; + + // Create path + if !self.database.path().exists() { + log::debug!("Creating CodeQL Database Path: {:?}", self.database.path()); + std::fs::create_dir_all(self.database.path())?; + } + + log::debug!("Creating CodeQL Database: {:?}", args); + self.codeql.run(args).await?; + Ok(()) + } + + fn create_cmd(&self) -> Result, GHASError> { + let mut args = vec!["database", "create"]; + + // Check if language is set + args.extend(vec!["-l", &self.database.language()]); + + // Add source root + if let Some(source) = &self.database.source { + args.extend(vec!["-s", source.to_str().expect("Invalid Source Root")]); + } else { + return Err(GHASError::CodeQLDatabaseError( + "No source root provided".to_string(), + )); + } + // Threat Models + let tms = self.threat_models.join(","); + if !tms.is_empty() { + args.push("--threat-models"); + args.push(&tms); + } + // Model Packs + let mps = self.model_packs.join(","); + if !mps.is_empty() { + args.push("--model-packs"); + args.push(&mps); + } + // Add Search Paths + let search_paths = self.codeql.search_paths(); + if !search_paths.is_empty() { + args.push("--search-path"); + args.push(&search_paths); + } + + // Overwrite the database if it exists + if self.overwrite { + args.push("--overwrite"); + } + if let Some(category) = &self.category { + args.push("--sarif-category"); + args.push(&category); + } + + // Add the path to the database + let path = self.database.path.to_str().expect("Invalid Database Path"); + args.push(path); + + Ok(args.iter().map(|s| s.to_string()).collect()) + } + + pub(crate) fn default_results(database: &CodeQLDatabase) -> PathBuf { + let mut path = CodeQLDatabases::default_results(); + + if let Some(ref repo) = database.repository { + path.push(format!( + "{}-{}-{}.sarif", + database.language(), + repo.owner(), + repo.name(), + )); + } else { + path.push(format!( + "{}-{}.sarif", + database.language.language(), + database.name + )); + } + + path + } + + /// Analyze the database + pub async fn analyze(&self) -> Result<(), GHASError> { + log::debug!("Analyzing CodeQL Database: {:?}", self.database); + + let args = self.analyze_cmd()?; + + log::debug!("Analyzing CodeQL Command :: {:?}", args); + + self.codeql.run(args).await?; + + log::debug!("CodeQL Database Analysis Complete"); + log::debug!("Output Path: {:?}", self.output); + Ok(()) + } + + fn analyze_cmd(&self) -> Result, GHASError> { + let mut args = vec!["database", "analyze"]; + + // Output and Format + if let Some(path) = &self.output.to_str() { + args.extend(vec!["--output", path]); + } else { + return Err(GHASError::CodeQLDatabaseError( + "No output path provided".to_string(), + )); + } + args.extend(vec!["--format", self.output_format.as_str()]); + + // Search Paths + let search_paths = self.codeql.search_paths(); + if !search_paths.is_empty() { + args.push("--search-path"); + args.push(&search_paths); + } + // Summary + if self.summary { + args.push("--print-diagnostics-summary"); + args.push("--print-metrics-summary"); + } + + // Add the path to the database + let path = self.database.path.to_str().expect("Invalid Database Path"); + args.push(path); + + // Add the queries + let queries = self.queries.to_string(); + args.push(queries.as_str()); + + Ok(args.iter().map(|s| s.to_string()).collect()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CodeQL, CodeQLDatabase}; + + fn init_codeql() -> (CodeQL, CodeQLDatabase) { + let codeql = CodeQL::default(); + let database = CodeQLDatabase::init() + .name("test") + .language("javascript") + .source(PathBuf::from("/path/to/source")) + .build() + .unwrap(); + (codeql, database) + } + + #[test] + fn test_codeql_create() { + let (codeql, database) = init_codeql(); + let cmd = CodeQLDatabaseHandler::new(&database, &codeql) + .sarif(PathBuf::from("test.sarif")) + .create_cmd() + .unwrap(); + + assert_eq!(cmd.len(), 7); + assert_eq!(cmd[0], "database"); + assert_eq!(cmd[1], "create"); + assert_eq!(cmd[2], "-l"); + assert_eq!(cmd[3], "javascript"); + assert_eq!(cmd[4], "-s"); + assert_eq!(cmd[5], "/path/to/source"); + // Summary enabled by default + assert_eq!(cmd[6], database.path().to_str().unwrap()); + } + + #[test] + fn test_codeql_create_threat_model() { + let (codeql, database) = init_codeql(); + let cmd = CodeQLDatabaseHandler::new(&database, &codeql) + .threat_model("test".to_string()) + .create_cmd() + .unwrap(); + + assert_eq!(cmd.len(), 9); + assert_eq!(cmd[6], "--threat-models"); + assert_eq!(cmd[7], "test"); + + let cmd = CodeQLDatabaseHandler::new(&database, &codeql) + .threat_models(vec!["test".to_string(), "test2".to_string()]) + .create_cmd() + .unwrap(); + + assert_eq!(cmd.len(), 9); + assert_eq!(cmd[6], "--threat-models"); + assert_eq!(cmd[7], "test,test2"); + + let cmd = CodeQLDatabaseHandler::new(&database, &codeql) + .disable_default_threat_model() + .create_cmd() + .unwrap(); + assert_eq!(cmd.len(), 9); + assert_eq!(cmd[6], "--threat-models"); + assert_eq!(cmd[7], "!default"); + } + + #[test] + fn test_codeql_create_pack_models() { + let (codeql, database) = init_codeql(); + let cmd = CodeQLDatabaseHandler::new(&database, &codeql) + .model_packs(vec!["test".to_string(), "test2".to_string()]) + .create_cmd() + .unwrap(); + + assert_eq!(cmd.len(), 9); + assert_eq!(cmd[6], "--model-packs"); + assert_eq!(cmd[7], "test,test2"); + } + + #[test] + fn test_codeql_analysis() { + let (codeql, database) = init_codeql(); + let cmd = CodeQLDatabaseHandler::new(&database, &codeql) + .sarif("test.sarif") + .analyze_cmd() + .unwrap(); + + assert_eq!(cmd.len(), 10); + assert_eq!(cmd[0], "database"); + assert_eq!(cmd[1], "analyze"); + assert_eq!(cmd[2], "--output"); + assert_eq!(cmd[3], "test.sarif"); + assert_eq!(cmd[4], "--format"); + assert_eq!(cmd[5], "sarif-latest"); + assert_eq!(cmd[6], "--print-diagnostics-summary"); + assert_eq!(cmd[7], "--print-metrics-summary"); + assert_eq!(cmd[8], database.path().to_str().unwrap()); + assert_eq!(cmd[9], "codeql/javascript-queries"); + } + + #[test] + fn test_codeql_analysis_suites() { + let (codeql, database) = init_codeql(); + let cmd = CodeQLDatabaseHandler::new(&database, &codeql) + .suite("javascript") + .analyze_cmd() + .unwrap(); + assert_eq!(cmd.len(), 10); + assert_eq!(cmd[9], "codeql/javascript-queries"); + + let cmd = CodeQLDatabaseHandler::new(&database, &codeql) + .suite("security-extended") + .analyze_cmd() + .unwrap(); + assert_eq!(cmd.len(), 10); + assert_eq!( + cmd[9], + "codeql/javascript-queries:codeql-suites/javascript-security-extended.qls" + ); + } + + #[test] + fn test_codeql_analysis_queries() { + let (codeql, database) = init_codeql(); + let cmd = CodeQLDatabaseHandler::new(&database, &codeql) + .queries("codeql/javascript-queries@0.9.0") + .analyze_cmd() + .unwrap(); + assert_eq!(cmd.len(), 10); + assert_eq!(cmd[9], "codeql/javascript-queries@0.9.0"); + + let cmd = CodeQLDatabaseHandler::new(&database, &codeql) + .queries("codeql/javascript-queries@0.9.0:codeql-suites/javascript-code-scanning.qls") + .analyze_cmd() + .unwrap(); + assert_eq!(cmd.len(), 10); + assert_eq!( + cmd[9], + "codeql/javascript-queries@0.9.0:codeql-suites/javascript-code-scanning.qls" + ); + } +} diff --git a/vendor/ghastoolkit/src/codeql/database/queries.rs b/vendor/ghastoolkit/src/codeql/database/queries.rs new file mode 100644 index 0000000..7f22066 --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/database/queries.rs @@ -0,0 +1,237 @@ +//! # CodeQL Queries + +use std::path::PathBuf; + +use crate::{GHASError, codeql::languages::CODEQL_LANGUAGES}; + +/// A collection of CodeQL Queries +/// scope/name@range:path +#[derive(Debug, Default, Clone)] +pub struct CodeQLQueries { + pub(crate) scope: Option, + pub(crate) name: Option, + pub(crate) range: Option, + pub(crate) path: Option, +} + +impl CodeQLQueries { + /// Parse a string into a CodeQLQueries instance + pub fn parse(value: impl Into) -> Result { + let value = value.into(); + if value.is_empty() { + return Err(GHASError::CodeQLPackError( + "CodeQLQueries cannot be empty".to_string(), + )); + } + + // Absolute or relative path + if value.starts_with('/') || value.starts_with('.') { + Ok(Self { + path: Some(PathBuf::from(value)), + ..Default::default() + }) + } else { + let mut scope = None; + let mut name = None; + let mut range = None; + let mut path = None; + + if let Some((scp, nm)) = value.split_once('/') { + scope = Some(scp.to_string()); + + match nm.split_once('@') { + Some((n, rng)) => { + name = Some(n.to_string()); + match rng.split_once(':') { + Some((r, p)) => { + range = Some(r.to_string()); + path = Some(PathBuf::from(p)); + } + None => { + range = Some(rng.to_string()); + } + } + } + None => { + name = Some(nm.to_string()); + } + } + } else if CODEQL_LANGUAGES + .iter() + .find(|lang| lang.0 == value) + .is_some() + { + return Ok(Self::language_default(&value)); + } + + Ok(Self { + scope, + name, + range, + path, + }) + } + } + + /// Create new CodeQL Queries from language + pub fn language_default(language: &str) -> Self { + Self { + scope: Some("codeql".to_string()), + name: Some(format!("{language}-queries")), + ..Default::default() + } + } + + /// Name of the query + pub fn name(&self) -> Option { + self.name.clone() + } + + /// Get the scope/namespace of the query + pub fn scope(&self) -> Option { + self.scope.clone() + } + + /// Get the range/version of the query + pub fn range(&self) -> Option { + self.range.clone() + } + + /// Get the suite path + pub fn suite(&self) -> Option { + if let Some(path) = &self.path { + return Some(path.display().to_string()); + } + None + } + + /// Set a suite path + pub fn set_path(&mut self, path: impl Into) { + self.path = Some(path.into()); + } +} + +impl ToString for CodeQLQueries { + fn to_string(&self) -> String { + let mut query = String::new(); + + // Pack mode + if let Some(scope) = &self.scope { + query += scope; + } + if let Some(name) = &self.name { + query += "/"; + query += name; + } + + // Range + if let Some(range) = &self.range { + query += "@"; + query += range; + } + + // Path + if let Some(path) = &self.path { + if query.is_empty() { + query = path.to_str().unwrap().to_string(); + } else { + query += ":"; + query += path.to_str().unwrap(); + } + } + + query + } +} + +impl From<&str> for CodeQLQueries { + fn from(value: &str) -> Self { + Self::parse(value).unwrap_or_else(|_| { + log::error!("Failed to parse CodeQLQueries from '{}'", value); + Self::default() + }) + } +} + +impl From for CodeQLQueries { + fn from(value: String) -> Self { + Self::parse(&value).unwrap_or_else(|_| { + log::error!("Failed to parse CodeQLQueries from '{}'", value); + Self::default() + }) + } +} + +impl From<&String> for CodeQLQueries { + fn from(value: &String) -> Self { + Self::parse(value).unwrap_or_else(|_| { + log::error!("Failed to parse CodeQLQueries from '{}'", value); + Self::default() + }) + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use crate::codeql::database::queries::CodeQLQueries; + + #[test] + fn test_pack() { + let queries = CodeQLQueries::parse("codeql/python-queries") + .expect("Failed to parse CodeQLQueries from string"); + + assert_eq!(queries.scope, Some("codeql".to_string())); + assert_eq!(queries.name, Some("python-queries".to_string())); + assert_eq!(queries.range, None); + assert_eq!(queries.path, None); + } + + #[test] + fn test_language_default() { + let queries = CodeQLQueries::language_default("python"); + assert_eq!(queries.scope, Some("codeql".to_string())); + assert_eq!(queries.name, Some("python-queries".to_string())); + assert_eq!(queries.range, None); + assert_eq!(queries.path, None); + } + + #[test] + fn test_string() { + let query = CodeQLQueries { + scope: Some("codeql".to_string()), + name: Some("python-queries".to_string()), + range: Some("0.9.0".to_string()), + path: Some(PathBuf::from("codeql-suites/python-code-scanning.qls")), + }; + + assert_eq!( + query.to_string(), + "codeql/python-queries@0.9.0:codeql-suites/python-code-scanning.qls" + ); + } + + #[test] + fn test_pack_range() { + let queries = CodeQLQueries::from("codeql/python-queries@0.9.0"); + assert_eq!(queries.scope, Some("codeql".to_string())); + assert_eq!(queries.name, Some("python-queries".to_string())); + assert_eq!(queries.range, Some("0.9.0".to_string())); + assert_eq!(queries.path, None); + } + + #[test] + fn test_full() { + let queries = "codeql/python-queries@0.9.0:codeql-suites/python-code-scanning.qls"; + let codeql_queries = CodeQLQueries::from(queries); + + assert_eq!(codeql_queries.scope, Some(String::from("codeql"))); + assert_eq!(codeql_queries.name, Some(String::from("python-queries"))); + assert_eq!(codeql_queries.range, Some(String::from("0.9.0"))); + assert_eq!( + codeql_queries.path, + Some(PathBuf::from("codeql-suites/python-code-scanning.qls")) + ); + } +} diff --git a/vendor/ghastoolkit/src/codeql/databases.rs b/vendor/ghastoolkit/src/codeql/databases.rs new file mode 100644 index 0000000..e0c7a5b --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/databases.rs @@ -0,0 +1,304 @@ +//! # CodeQL Databases + +use log::debug; +use std::path::PathBuf; +use walkdir::WalkDir; + +use super::CodeQLLanguage; +use crate::{GHASError, GitHub}; +use crate::{Repository, codeql::database::CodeQLDatabase}; + +/// A list of CodeQL databases +#[derive(Debug, Clone)] +pub struct CodeQLDatabases { + /// The Base path for the databases + path: PathBuf, + /// The list of databases + databases: Vec, +} + +impl Iterator for CodeQLDatabases { + type Item = CodeQLDatabase; + + fn next(&mut self) -> Option { + self.databases.pop() + } +} + +impl CodeQLDatabases { + /// Create a new list of databases + pub fn new() -> Self { + Self { + path: CodeQLDatabases::default_path(), + databases: Vec::new(), + } + } + + /// Set the root path where the databases are stored + pub fn set_path(&mut self, path: impl Into) { + self.path = path.into(); + } + + /// Get all of the loaded databases + pub fn databases(&self) -> Vec { + self.databases.clone() + } + + /// Add a database to the list + pub fn add(&mut self, database: CodeQLDatabase) { + self.databases.push(database); + } + + /// Check if the list is empty + pub fn is_empty(&self) -> bool { + self.databases.is_empty() + } + + /// Get the number of databases in the list + pub fn len(&self) -> usize { + self.databases.len() + } + + /// Dowload a database from the GitHub API + pub(crate) async fn download_database( + output: &PathBuf, + repository: &Repository, + github: &GitHub, + language: &CodeQLLanguage, + ) -> Result { + if github.is_enterprise_server() { + return Err(GHASError::CodeQLDatabaseError( + "CodeQL database download is not supported on GitHub Enterprise Server".to_string(), + )); + } + + let dbpath = output.join("codeql-database.zip"); + let route = format!( + "{base}repos/{owner}/{repo}/code-scanning/codeql/databases/{language}", + base = github.base(), + owner = repository.owner(), + repo = repository.name(), + language = language.language() + ); + log::debug!("Route: {}", route); + + let client = reqwest::Client::new(); + let mut request = client + .get(route) + .header( + http::header::ACCEPT, + http::header::HeaderValue::from_str("application/zip")?, + ) + .header( + http::header::USER_AGENT, + http::header::HeaderValue::from_str("ghastoolkit")?, + ); + + if let Some(token) = github.token() { + request = request.header(http::header::AUTHORIZATION, format!("Bearer {}", token)); + } + + let data = request.send().await?.bytes().await?; + + tokio::fs::write(&dbpath, data).await?; + log::debug!("Database archive downloaded to {}", dbpath.display()); + + if !dbpath.exists() { + return Err(crate::GHASError::CodeQLDatabaseError(format!( + "Database not found at: {}", + output.display() + ))); + } + + log::debug!("Unzipping CodeQL database to {}", output.display()); + Self::unzip_codeql_database(&dbpath, &output)?; + + let mut db = CodeQLDatabase::load(output)?; + db.set_repository(repository); + + Ok(db) + } + + /// Download a database and store it in the CodeQL Databases path + pub async fn download( + &mut self, + repository: &Repository, + github: &GitHub, + ) -> Result, GHASError> { + let mut databases = Vec::new(); + let database_list = github + .code_scanning(repository) + .list_codeql_databases() + .await?; + + for dbitem in database_list { + let language = CodeQLLanguage::from(dbitem.language); + + let path = Self::default_db_path(&self.path, repository, language.language()); + if !path.exists() { + debug!("Creating database path: {}", path.display()); + tokio::fs::create_dir_all(&path).await?; + } + debug!("Downloading database to: {}", path.display()); + + let db = Self::download_database(&path, repository, github, &language).await?; + log::debug!("Database: {db:?}"); + + self.add(db.clone()); + databases.push(db); + } + + Ok(databases) + } + + /// Download a database for a specific language + pub async fn download_language( + &mut self, + repository: &Repository, + github: &GitHub, + language: impl Into, + ) -> Result { + let language = language.into(); + + let path = Self::default_db_path(&self.path, repository, language.language()); + if !path.exists() { + debug!("Creating database path: {}", path.display()); + tokio::fs::create_dir_all(&path).await?; + } + log::debug!("Downloading database to: {}", path.display()); + let db = Self::download_database(&path, repository, github, &language).await?; + log::debug!("Database: {db:?}"); + + self.add(db.clone()); + + Ok(db) + } + + /// Unzip the CodeQL database + fn unzip_codeql_database(zip: &PathBuf, output: &PathBuf) -> Result<(), GHASError> { + log::debug!("Unzipping CodeQL database to {}", output.display()); + let file = std::fs::File::open(zip)?; + let mut archive = zip::ZipArchive::new(file)?; + archive.extract(output)?; + + Ok(()) + } + + /// Create a default path for the database to be stored + pub(crate) fn default_db_path( + base: &PathBuf, + repo: &Repository, + language: impl Into, + ) -> PathBuf { + base.join(repo.owner()) + .join(repo.name()) + .join(language.into()) + } + + /// Get the default path for CodeQL databases + pub fn default_path() -> PathBuf { + // Get env var CODEQL_DATABASES + match std::env::var("CODEQL_DATABASES") { + Ok(p) => PathBuf::from(p), + Err(_) => { + // Get HOME directory + match std::env::var("HOME") { + Ok(p) => { + let mut base = PathBuf::from(p); + base.push(".codeql"); + base.push("databases"); + base + } + Err(_) => PathBuf::from("/tmp/codeql"), + } + } + } + } + + /// Get the default path for CodeQL results + pub fn default_results() -> PathBuf { + match std::env::var("CODEQL_RESULTS") { + Ok(p) => PathBuf::from(p), + Err(_) => match std::env::var("HOME") { + Ok(p) => { + let mut base = PathBuf::from(p); + base.push(".codeql"); + base.push("results"); + base + } + Err(_) => PathBuf::from("/tmp/codeql"), + }, + } + } + + /// Walk directory to find all CodeQL databases. + pub fn load(path: impl Into) -> CodeQLDatabases { + let path = path.into(); + debug!("Loading databases from: {}", path.display()); + + let mut databases = CodeQLDatabases::new(); + databases.path = path.clone(); + + WalkDir::new(path) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.file_name() == "codeql-database.yml") + .for_each(|path| { + let database = CodeQLDatabase::from(path.path()); + databases.add(database); + }); + + databases + } +} + +impl From for CodeQLDatabases { + fn from(path: String) -> Self { + CodeQLDatabases::load(path) + } +} + +impl From<&str> for CodeQLDatabases { + fn from(path: &str) -> Self { + CodeQLDatabases::load(path.to_string()) + } +} + +impl From for CodeQLDatabases { + fn from(path: PathBuf) -> Self { + CodeQLDatabases::load(path.to_str().expect("Invalid path").to_string()) + } +} + +impl Default for CodeQLDatabases { + fn default() -> Self { + let path = CodeQLDatabases::default_path() + .to_str() + .expect("Invalid path") + .to_string(); + CodeQLDatabases::load(format!("{}/**/codeql-database.yml", path)) + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use crate::CodeQLDatabases; + + #[test] + fn test_default_codeql_path() { + let home_path = match std::env::var("HOME") { + Ok(p) => { + let mut path = PathBuf::from(p); + path.push(".codeql"); + path.push("databases"); + path + } + Err(_) => PathBuf::from("/tmp/codeql"), + }; + let path = CodeQLDatabases::default_path(); + + assert_eq!(path, home_path); + } +} diff --git a/vendor/ghastoolkit/src/codeql/download.rs b/vendor/ghastoolkit/src/codeql/download.rs new file mode 100644 index 0000000..0d6d5da --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/download.rs @@ -0,0 +1,146 @@ +//! # CodeQL CLI Download +//! +//! The `CodeQL` struct provides methods to download the CodeQL CLI from GitHub +//! using the GitHub Actions Tool Cache. + +use super::{CodeQL, CodeQLVersion}; +use crate::GHASError; +use ghactions::{ToolCache, ToolPlatform}; + +impl CodeQL { + /// Download the CodeQL CLI from GitHub using the GitHub Actions Tool Cache + pub async fn download(client: &octocrab::Octocrab) -> Result { + let mut codeql = CodeQL::default(); + codeql + .download_version(client, CodeQLVersion::Latest) + .await?; + Ok(codeql) + } + + /// Check and install the CodeQL CLI if not already installed + pub async fn install( + &mut self, + client: &octocrab::Octocrab, + version: impl Into, + ) -> Result<(), GHASError> { + if !self.is_installed().await { + self.download_version(client, version).await?; + } + Ok(()) + } + + /// Download the latest version of the CodeQL CLI from GitHub + pub async fn download_latest(&mut self, client: &octocrab::Octocrab) -> Result<(), GHASError> { + self.download_version(client, "latest").await + } + + /// Download the CodeQL CLI from GitHub using the GitHub Actions Tool Cache + pub async fn download_version( + &mut self, + client: &octocrab::Octocrab, + version: impl Into, + ) -> Result<(), GHASError> { + let version = version.into(); + + let toolcache = ToolCache::new(); + let path = toolcache.new_tool_path("codeql", version.to_string().as_str()); + + let codeql_archive = path.join("codeql.zip"); + log::debug!("CodeQL CLI archive path: {:?}", codeql_archive); + log::debug!("CodeQL CLI directory path: {:?}", path); + + // CodeQL CLI names for the different platforms + let platform = CodeQL::codeql_platform_str(&toolcache)?; + log::debug!("CodeQL CLI platform: {}", platform); + + if !codeql_archive.exists() { + let release = CodeQL::get_codeql_release(client, version).await?; + log::debug!("CodeQL CLI version {} found on GitHub", release.tag_name); + + let codeql_str = format!("{}.zip", platform); + log::debug!("CodeQL CLI asset name: {}", codeql_str); + let Some(asset) = release + .assets + .iter() + .find(|a| a.name == codeql_str.as_str()) + else { + return Err(GHASError::CodeQLError( + "CodeQL CLI asset not found".to_string(), + )); + }; + + if let Some(parent) = codeql_archive.parent() { + log::debug!("Creating parent directory: {:?}", parent); + std::fs::create_dir_all(parent)?; + } + + log::info!( + "Downloading CodeQL CLI from GitHub: {}", + asset.browser_download_url + ); + toolcache.download_asset(&asset, &codeql_archive).await?; + } + + log::info!("Extracting asset to {:?}", path); + toolcache.extract_archive(&codeql_archive, &path).await?; + + let codeql_dir = path.join("codeql"); + if !codeql_dir.exists() { + return Err(GHASError::CodeQLError( + "CodeQL CLI directory not found".to_string(), + )); + } + log::info!("CodeQL CLI extracted to {:?}", codeql_dir); + + self.set_path(codeql_dir.join("codeql")); + Ok(()) + } + + async fn get_codeql_release( + client: &octocrab::Octocrab, + version: impl Into, + ) -> Result { + let version = version.into(); + + match version { + CodeQLVersion::Nightly => { + log::debug!("Fetching nightly CodeQL CLI release"); + Ok(client + .repos("dsp-testing", "codeql-cli-nightlies") + .releases() + .get_latest() + .await?) + } + CodeQLVersion::Latest => { + log::debug!("Fetching latest CodeQL CLI release"); + Ok(client + .repos("github", "codeql-cli-binaries") + .releases() + .get_latest() + .await?) + } + CodeQLVersion::Version(ver) => { + log::debug!("Fetching CodeQL CLI release by tag: {}", ver); + Ok(client + .repos("github", "codeql-cli-binaries") + .releases() + .get_by_tag(ver.as_str()) + .await?) + } + } + } + + /// Convert the toolcache platform to a string for the CodeQL CLI + fn codeql_platform_str(toolcache: &ToolCache) -> Result<&str, GHASError> { + Ok(match toolcache.platform() { + ToolPlatform::Linux => "codeql-linux64", + ToolPlatform::MacOS => "codeql-osx64", + ToolPlatform::Windows => "codeql-win64", + _ => { + return Err(GHASError::CodeQLError( + "Unsupported platform for CodeQL CLI".to_string(), + )); + } + }) + } +} diff --git a/vendor/ghastoolkit/src/codeql/languages.rs b/vendor/ghastoolkit/src/codeql/languages.rs new file mode 100644 index 0000000..9752656 --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/languages.rs @@ -0,0 +1,176 @@ +//! # CodeQL Languages +use std::fmt::{Debug, Display}; +use std::path::PathBuf; + +use super::CodeQLExtractor; + +/// CodeQL Languages mappings +pub const CODEQL_LANGUAGES: [(&str, &str); 16] = [ + ("actions", "GitHub Actions"), + ("c", "C/C++"), + ("cpp", "C/C++"), + ("c-cpp", "C/C++"), + ("csharp", "C#"), + ("java", "Java/Kotlin"), + ("kotlin", "Java/Kotlin"), + ("java-kotlin", "Java/Kotlin"), + ("javascript", "Javascript/Typescript"), + ("typescript", "Javascript/Typescript"), + ("javascript-typescript", "Javascript/Typescript"), + ("python", "Python"), + ("go", "Go"), + ("rust", "Rust"), + ("ruby", "Rudy"), + ("swift", "Swift"), +]; + +/// CodeQL Languages +#[derive(Debug, Clone, Default)] +pub struct CodeQLLanguages { + languages: Vec, +} + +impl CodeQLLanguages { + /// Create a new instance of CodeQLLanguages + pub fn new(languages: Vec) -> Self { + CodeQLLanguages { languages } + } + /// Check if a language is supported by CodeQL + pub fn check(&self, language: impl Into) -> bool { + let language = language.into(); + for lang in &self.languages { + if lang.extractor.languages().contains(&language) { + return true; + } + } + false + } + + /// Get all languages supported by CodeQL + pub fn get_all(&self) -> &Vec { + &self.languages + } + /// Get all primary languages supported by CodeQL + pub fn get_languages(&self) -> Vec { + self.languages + .iter() + .filter(|l| !l.is_secondary()) + .cloned() + .collect() + } + /// Get all secondary languages supported by CodeQL + pub fn get_secondary(&self) -> Vec { + self.languages + .iter() + .filter(|l| l.is_secondary()) + .cloned() + .collect() + } +} + +/// Languages supported by CodeQL. +#[derive(Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct CodeQLLanguage { + name: String, + extractor: CodeQLExtractor, +} + +impl CodeQLLanguage { + /// Get the pretty name of the language + pub fn pretty(&self) -> &str { + if CODEQL_LANGUAGES.iter().any(|(lang, _)| lang == &self.name) { + CODEQL_LANGUAGES + .iter() + .find(|(lang, _)| lang == &self.name) + .unwrap() + .1 + } else if !self.extractor.name.is_empty() { + &self.extractor.name + } else { + &self.name + } + } + + /// Get the language string for CodeQL (aliases are supported) + pub fn language(&self) -> &str { + if !self.extractor.name.is_empty() { + &self.extractor.name + } else { + &self.name + } + } + + /// Check if the language is a secondary language + pub fn is_secondary(&self) -> bool { + matches!( + self.extractor.name.as_str(), + "properties" | "csv" | "yaml" | "xml" | "html" + ) + } +} + +impl Display for CodeQLLanguage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.pretty()) + } +} + +impl Debug for CodeQLLanguage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.is_secondary() { + write!(f, "Secondary('{}')", self.pretty()) + } else { + write!(f, "Primary('{}')", self.pretty()) + } + } +} + +impl From<(String, PathBuf)> for CodeQLLanguage { + fn from(value: (String, PathBuf)) -> Self { + CodeQLLanguage { + name: value.0.clone(), + extractor: CodeQLExtractor::load_path(value.1.clone()).unwrap(), + } + } +} + +impl From for CodeQLLanguage { + fn from(value: String) -> Self { + CodeQLLanguage { + name: value.clone(), + extractor: CodeQLExtractor::default(), + } + } +} + +impl From<&str> for CodeQLLanguage { + fn from(value: &str) -> Self { + CodeQLLanguage { + name: value.to_string(), + extractor: CodeQLExtractor::default(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_languages() { + let language = CodeQLLanguage::from("cpp".to_string()); + assert_eq!(language.language(), "cpp"); + assert_eq!(language.pretty(), "C/C++"); + + let language = CodeQLLanguage::from("actions"); + assert_eq!(language.language(), "actions"); + assert_eq!(language.pretty(), "GitHub Actions"); + } + + #[test] + fn test_unsupported() { + let language = CodeQLLanguage::from("iac"); + assert_eq!(language.language(), "iac"); + assert_eq!(language.pretty(), "iac"); + } +} diff --git a/vendor/ghastoolkit/src/codeql/mod.rs b/vendor/ghastoolkit/src/codeql/mod.rs new file mode 100644 index 0000000..30be0ed --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/mod.rs @@ -0,0 +1,92 @@ +//! # CodeQL +//! +//! This module contains a simple interface to work with CodeQL CLI and databases in Rust. +//! +//! ## Usage +//! +//! ```no_run +//! use ghastoolkit::codeql::{CodeQL, CodeQLDatabase, CodeQLDatabases}; +//! +//! # #[tokio::main] +//! # async fn main() { +//! // Setup a default CodeQL CLI +//! let codeql = CodeQL::new().await; +//! println!("CodeQL :: {}", codeql); +//! +//! // Get all CodeQL databases from the default path +//! let databases = CodeQLDatabases::default(); +//! +//! for database in databases { +//! println!("Database :: {}", database); +//! } +//! # } +//! ``` +//! +//! You can also use the builder pattern to create a new CodeQL CLI instance: +//! +//! ```rust +//! use ghastoolkit::codeql::CodeQL; +//! +//! # #[tokio::main] +//! # async fn main() { +//! let codeql = CodeQL::init() +//! .path(String::from("/path/to/codeql")) +//! .threads(4) +//! .ram(8000) +//! .build() +//! .await +//! .expect("Failed to create CodeQL instance"); +//! # } +//! ``` +//! +//! ## CodeQL Database +//! +//! If you want to create and analyze a CodeQL database, you can use the `CodeQLDatabase` struct: +//! +//! ```no_run +//! use ghastoolkit::codeql::{CodeQL, CodeQLDatabase}; +//! +//! # #[tokio::main] +//! # async fn main() { +//! let codeql = CodeQL::default(); +//! +//! // Create a new CodeQL database +//! let database = CodeQLDatabase::init() +//! .name("ghastoolkit") +//! .language("python") +//! .source(String::from("/path/to/source")) +//! .build() +//! .expect("Failed to create CodeQL database"); +//! +//! println!("Database :: {}", database); +//! +//! // Create a new CodeQL database +//! codeql.database(&database) +//! .overwrite() +//! .create() +//! .await +//! .expect("Failed to create CodeQL database"); +//! +//! let results = codeql.database(&database) +//! .analyze() +//! .await +//! .expect("Failed to analyze CodeQL database"); +//! # } +//!``` + +pub mod cli; +pub mod database; +pub mod databases; +#[cfg(feature = "toolcache")] +pub mod download; +pub mod extractors; +pub mod languages; +pub mod packs; +pub mod version; + +pub use cli::CodeQL; +pub use database::CodeQLDatabase; +pub use databases::CodeQLDatabases; +pub use extractors::CodeQLExtractor; +pub use languages::CodeQLLanguage; +pub use version::CodeQLVersion; diff --git a/vendor/ghastoolkit/src/codeql/packs/handler.rs b/vendor/ghastoolkit/src/codeql/packs/handler.rs new file mode 100644 index 0000000..57ee836 --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/packs/handler.rs @@ -0,0 +1,84 @@ +//! # CodeQL Pack Handler +//! +//! This module provides a handler for managing CodeQL packs, which are collections of queries and databases used in CodeQL analysis. +use super::CodeQLPack; +use crate::{CodeQL, GHASError, codeql::database::queries::CodeQLQueries}; + +/// CodeQL Database Handler +#[derive(Debug, Clone)] +pub struct CodeQLPackHandler<'db, 'ql> { + /// Reference to the CodeQL Database + pack: &'db CodeQLPack, + /// Reference to the CodeQL instance + codeql: &'ql CodeQL, + /// Optional suite name for the pack + suite: Option, +} + +impl<'db, 'ql> CodeQLPackHandler<'db, 'ql> { + /// Creates a new CodeQLPackHandler with the given pack and CodeQL instance. + pub fn new(pack: &'db CodeQLPack, codeql: &'ql CodeQL) -> Self { + Self { + pack, + codeql, + suite: None, + } + } + + /// Sets the suite name for the pack handler. + pub fn suite(mut self, suite: impl Into) -> Self { + self.suite = match CodeQLQueries::parse(suite.into()) { + Ok(q) => q.suite(), + Err(e) => Some(e.to_string()), + }; + self + } + + fn get_suite(&self) -> Option { + if let Some(suite) = &self.suite { + return Some(suite.clone()); + } else if let Some(suite) = &self.codeql.suite { + return Some(suite.clone()); + } + None + } + + /// Resolves the queries in the pack and returns a list of query names grouped by language. + /// + /// The query path / name is returned relative to the pack path. + pub async fn resolve(&mut self) -> Result, GHASError> { + let mut args = vec!["resolve", "queries", "--format=json"]; + + let name = if let Some(suite) = &self.get_suite() { + format!("{}:{}", self.pack.full_name(), suite) + } else { + self.pack.full_name() + }; + args.push(name.as_str()); + log::debug!("Resolving queries for pack: {}", name); + println!("Resolving queries for pack: {}", name); + + let output = self.codeql.run(args).await?; + let json: Vec = serde_json::from_str(&output)?; + + let mut pack_path = self.pack.path().display().to_string(); + if !pack_path.ends_with('/') { + pack_path.push('/'); + } + + // Remove the pack path + Ok(json + .iter() + .map(|query| query.replace(&pack_path, "")) + .collect()) + } + + /// Downloads the CodeQL pack. + pub async fn download(&self) -> Result<(), GHASError> { + log::debug!("Downloading CodeQL Pack: {}", self.pack.full_name()); + self.codeql + .run(vec!["pack", "download", self.pack.full_name().as_str()]) + .await?; + Ok(()) + } +} diff --git a/vendor/ghastoolkit/src/codeql/packs/loader.rs b/vendor/ghastoolkit/src/codeql/packs/loader.rs new file mode 100644 index 0000000..9e4be60 --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/packs/loader.rs @@ -0,0 +1,210 @@ +//! # CodeQLPack Loader +//! +//! This module provides functionality to load CodeQL Packs from various sources, including local directories and remote repositories. +use std::path::PathBuf; + +use crate::codeql::database::queries::CodeQLQueries; +use crate::codeql::packs::{PackYaml, PackYamlLock}; +use crate::{CodeQLPacks, GHASError}; + +/// # CodeQLPack Loader +use super::pack::CodeQLPack; + +impl CodeQLPack { + /// Load a QLPack from a path (root directory or qlpack.yml file) + pub fn load(path: impl Into) -> Result { + // Path is the directory + let mut path: PathBuf = path.into(); + log::debug!("Loading CodeQL Pack from path: {}", path.display()); + + if !path.exists() { + return Err(GHASError::CodeQLPackError(path.display().to_string())); + } + if path.is_file() { + // TODO: Is this the best way to handle this? + path = path.parent().unwrap().to_path_buf(); + } + + let qlpack_path = path.join("qlpack.yml"); + let qlpack_lock_path = path.join("codeql-pack.lock.yml"); + + if !qlpack_path.exists() { + return Err(GHASError::CodeQLPackError( + "qlpack.yml file does not exist".to_string(), + )); + } + + let pack: PackYaml = match serde_yaml::from_reader(std::fs::File::open(&qlpack_path)?) { + Ok(p) => p, + Err(e) => return Err(GHASError::YamlError(e)), + }; + let pack_type = Self::get_pack_type(&pack); + + let pack_lock: Option = match std::fs::File::open(qlpack_lock_path) { + Ok(f) => match serde_yaml::from_reader(f) { + Ok(p) => Some(p), + Err(e) => return Err(GHASError::YamlError(e)), + }, + Err(_) => None, + }; + + let queries = CodeQLQueries::from(&pack.name.clone()); + Ok(Self { + queries, + path, + pack: Some(pack), + pack_type: Some(pack_type), + pack_lock, + }) + } + + pub(crate) fn load_remote_pack(remote: impl Into) -> Result { + let queries = CodeQLQueries::from(remote.into()); + + // Load the pack from the CodeQL Packages Directory + if let Ok(pack) = Self::load_package( + queries.name().unwrap_or_default(), + queries.scope().unwrap_or_default(), + queries.range(), + ) { + Ok(pack) + } else { + Ok(Self { + queries, + ..Default::default() + }) + } + } + + /// Load a CodeQL Pack from the CodeQL Packages Directory + /// + /// It will try to load the pack from the specified namespace, name, and optinal version. + pub(crate) fn load_package( + name: impl Into, + namespace: impl Into, + version: Option, + ) -> Result { + let name = name.into(); + let namespace = namespace.into(); + let queries = CodeQLQueries { + name: Some(name.clone()), + scope: Some(namespace.clone()), + range: version.clone(), + path: None, + }; + let version_num = if let Some(ref version) = version { + version.clone() + } else { + "**".to_string() + }; + + let path = CodeQLPacks::codeql_packages_path() + .join(&namespace) + .join(&name) + .join(version_num); + log::debug!("Loading CodeQL Pack from path: {}", path.display()); + + let qlpack_path = path.join("qlpack.yml"); + + if qlpack_path.exists() { + log::debug!("Loading pack from path: {}", path.display()); + return Self::load(path); + } + + // Multiple versions of the same pack can exist in the same directory + // Find the last / newest version + if let Ok(entries) = glob::glob(&qlpack_path.display().to_string()) { + log::trace!("Entries: {:?}", entries); + + if let Some(Ok(entry)) = entries.last() { + if entry.exists() { + return Self::load(entry.clone()); + } + } + } + + // If the path does not exist, return a CodeQL Pack with the name, namespace, and version + Ok(Self { + queries, + path: PathBuf::new(), + ..Default::default() + }) + } +} + +impl TryFrom<&str> for CodeQLPack { + type Error = GHASError; + + fn try_from(value: &str) -> Result { + if let Ok(path) = PathBuf::from(value).canonicalize() { + log::debug!("Loading CodeQL Pack from path: {}", path.display()); + + if path.exists() { + return Self::load(path.clone()); + } + } + Self::load_remote_pack(value) + } +} + +impl TryFrom for CodeQLPack { + type Error = GHASError; + + fn try_from(value: String) -> Result { + Self::try_from(value.as_str()) + } +} + +impl TryFrom for CodeQLPack { + type Error = GHASError; + + fn try_from(value: PathBuf) -> Result { + Self::load(value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::CodeQLPackType; + use anyhow::Context; + + fn qlpacks() -> PathBuf { + // Examples + let mut path = std::env::current_dir().unwrap(); + path.push("../examples/codeql-packs/java/src"); + path.canonicalize().unwrap() + } + + #[test] + fn test_codeql_pack() { + let pack = CodeQLPack::try_from("codeql/python-queries") + .expect("Failed to create CodeQLPack from string"); + + assert_eq!(pack.name(), "python-queries"); + assert_eq!(pack.namespace(), "codeql"); + + let pack = CodeQLPack::try_from("codeql/python-queries@1.0.0") + .expect("Failed to create CodeQLPack from string with version"); + + assert_eq!(pack.name(), "python-queries"); + assert_eq!(pack.namespace(), "codeql"); + assert_eq!(pack.version(), Some("1.0.0".to_string())); + } + + #[test] + fn test_codeql_pack_path() { + let path = qlpacks(); + assert!(path.exists()); + + let pack = CodeQLPack::try_from(path.clone()) + .context(format!("Failed to load pack from path: {}", path.display())) + .unwrap(); + + assert_eq!(pack.path(), path); + assert_eq!(pack.name(), "codeql-java"); + assert_eq!(pack.namespace(), "geekmasher"); + assert_eq!(pack.version(), Some("1.0.0".to_string())); + assert_eq!(pack.pack_type(), CodeQLPackType::Queries); + } +} diff --git a/vendor/ghastoolkit/src/codeql/packs/mod.rs b/vendor/ghastoolkit/src/codeql/packs/mod.rs new file mode 100644 index 0000000..dfe6f46 --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/packs/mod.rs @@ -0,0 +1,11 @@ +//! CodeQL Packs + +pub mod handler; +pub mod loader; +pub mod models; +pub mod pack; +pub mod packs; + +pub use models::{CodeQLPackType, PackYaml, PackYamlLock}; +pub use pack::CodeQLPack; +pub use packs::CodeQLPacks; diff --git a/vendor/ghastoolkit/src/codeql/packs/models.rs b/vendor/ghastoolkit/src/codeql/packs/models.rs new file mode 100644 index 0000000..efcc61e --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/packs/models.rs @@ -0,0 +1,81 @@ +//! # CodeQL Packs Models +use serde::Deserialize; +use std::{collections::HashMap, fmt::Display}; + +/// CodeQL Pack Type +#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum CodeQLPackType { + /// CodeQL Library + Library, + /// CodeQL Queries + #[default] + Queries, + /// CodeQL Models + Models, + /// CodeQL Testing + Testing, +} + +impl Display for CodeQLPackType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CodeQLPackType::Library => write!(f, "Library"), + CodeQLPackType::Queries => write!(f, "Queries"), + CodeQLPackType::Models => write!(f, "Models"), + CodeQLPackType::Testing => write!(f, "Testing"), + } + } +} + +/// CodeQL Pack Yaml Structure +#[derive(Debug, Clone, Default, Deserialize)] +pub struct PackYaml { + /// The Pack Name + pub name: String, + /// Pack is a Library or not + pub library: Option, + /// The Pack Version + pub version: Option, + /// Pack Groups + pub groups: Option>, + /// The Pack Dependencies + pub dependencies: Option>, + + /// The Pack Suites + pub suites: Option, + /// The Pack Default Suite File + #[serde(rename = "defaultSuiteFile")] + pub default_suite_file: Option, + + /// The Pack Extractor name + pub extractor: Option, + + /// Extension Targets + #[serde(rename = "extensionTargets")] + pub extension_targets: Option>, + /// Data Extensions + #[serde(rename = "dataExtensions")] + pub data_extensions: Option>, + + /// The Pack Tests Directory + pub tests: Option, +} + +/// CodeQL Pack Lock Yaml Structure +#[derive(Debug, Clone, Default, Deserialize)] +pub struct PackYamlLock { + /// Lock Version + #[serde(rename = "lockVersion")] + pub lock_version: String, + /// Dependencies + pub dependencies: HashMap, + /// If the pack is compiled + pub compiled: bool, +} + +/// CodeQL Pack Lock Dependency +#[derive(Debug, Clone, Default, Deserialize)] +pub struct PackYamlLockDependency { + /// Version + pub version: String, +} diff --git a/vendor/ghastoolkit/src/codeql/packs/pack.rs b/vendor/ghastoolkit/src/codeql/packs/pack.rs new file mode 100644 index 0000000..0e2e6ca --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/packs/pack.rs @@ -0,0 +1,245 @@ +//! CodeQL Pack +use std::{collections::HashMap, fmt::Display, path::PathBuf}; + +use crate::GHASError; +use crate::codeql::CodeQLLanguage; +use crate::codeql::database::queries::CodeQLQueries; + +use super::models::CodeQLPackType; +use super::{PackYaml, PackYamlLock}; + +/// CodeQL Pack +#[derive(Debug, Clone, Default)] +pub struct CodeQLPack { + /// CodeQL Queries reference + pub(crate) queries: CodeQLQueries, + + /// Path + pub(crate) path: PathBuf, + /// Pack Yaml + pub(crate) pack: Option, + /// Pack Type + pub(crate) pack_type: Option, + /// Pack Lock + pub(crate) pack_lock: Option, +} + +impl CodeQLPack { + /// Create a new CodeQL Pack + pub fn new(pack: impl Into) -> Self { + let pack = pack.into(); + if let Ok(path) = PathBuf::from(&pack).canonicalize() { + return Self::load(path).unwrap_or_default(); + } else { + Self::load_remote_pack(pack).unwrap_or_default() + } + } + + /// Get the pack name + pub fn name(&self) -> String { + self.queries.name().unwrap_or_default() + } + + /// Get the pack namespace + pub fn namespace(&self) -> String { + self.queries.scope().unwrap_or_else(|| "codeql".to_string()) + } + + /// Get full name (namespace/name[@version][:suite]) + pub fn full_name(&self) -> String { + let mut full_name = format!("{}/{}", self.namespace(), self.name()); + if let Some(version) = self.queries.range() { + full_name.push_str(&format!("@{}", version)); + } + if let Some(suite) = self.queries.suite() { + full_name.push_str(&format!(":{}", suite)); + } + + full_name + } + + /// Get the root path of the CodeQL Pack + pub fn path(&self) -> PathBuf { + self.path.clone() + } + + /// Get the pack version + pub fn version(&self) -> Option { + if let Some(version) = &self.queries.range() { + return Some(version.clone()); + } else if let Some(pack) = &self.pack { + return pack.version.clone(); + } + None + } + + /// Get the pack language based on the extractor or extension targets + pub fn language(&self) -> Option { + if let Some(pack) = &self.pack { + if let Some(extractor) = &pack.extractor { + return Some(CodeQLLanguage::from(extractor.as_str())); + } else if let Some(targets) = &pack.extension_targets { + if let Some((_, lang)) = targets.iter().next() { + return Some(CodeQLLanguage::from(lang.as_str())); + } + } + } + None + } + + /// Get the default suite file for the pack + pub fn suite(&self) -> Option { + if let Some(pack) = &self.pack { + return pack.default_suite_file.clone(); + } + None + } + + /// Check if the pack is installed + pub async fn is_installed(&self) -> bool { + self.path.exists() + } + + /// Get the list of dependencies for the pack. + /// + /// If the Pack Lock is available, it will return the dependencies from the lock file. + /// Otherwise, it will return the dependencies from the pack file. + pub fn dependencies(&self) -> HashMap { + if let Some(pack_lock) = &self.pack_lock { + pack_lock + .dependencies + .iter() + .map(|(key, value)| (key.clone(), value.version.clone())) + .collect() + } else if let Some(pack) = &self.pack { + pack.dependencies.clone().unwrap_or_default() + } else { + HashMap::new() + } + } + /// Get the pack type + pub fn pack_type(&self) -> CodeQLPackType { + self.pack_type.clone().unwrap_or_default() + } + + /// Download a CodeQL Pack using its name (namespace/name[@version]) + /// + /// ```bash + /// codeql pack download + /// ``` + #[cfg(feature = "async")] + pub async fn download(&self, codeql: &crate::CodeQL) -> Result<(), GHASError> { + log::debug!("Downloading CodeQL Pack: {}", self.full_name()); + codeql + .run(vec!["pack", "download", self.full_name().as_str()]) + .await?; + Ok(()) + } + + /// Download a CodeQL Pack using its name (namespace/name[@version]) + pub async fn download_pack( + codeql: &crate::CodeQL, + name: impl Into, + ) -> Result { + let name = name.into(); + log::debug!("Downloading CodeQL Pack: {name}"); + let pack = CodeQLPack::try_from(name.clone())?; + pack.download(codeql).await?; + + Ok(pack) + } + + /// Install the CodeQL Pack Dependencies + /// + /// ```bash + /// codeql pack install + /// ``` + #[cfg(feature = "async")] + pub async fn install(&self, codeql: &crate::CodeQL) -> Result<(), GHASError> { + codeql + .run(vec!["pack", "install", self.path().to_str().unwrap()]) + .await + .map(|_| ()) + } + + /// Upgrade CodeQL Pack Dependencies + #[cfg(feature = "async")] + pub async fn upgrade(&self, codeql: &crate::CodeQL) -> Result<(), GHASError> { + codeql + .run(vec!["pack", "upgrade", self.path().to_str().unwrap()]) + .await + .map(|_| ()) + } + + /// Publish the CodeQL Pack + /// + /// ```bash + /// codeql pack publish + /// ``` + #[cfg(feature = "async")] + pub async fn publish( + &self, + codeql: &crate::CodeQL, + token: impl Into, + ) -> Result<(), GHASError> { + Ok(tokio::process::Command::new(codeql.path()) + .env("CODEQL_REGISTRIES_AUTH", token.into()) + .args(vec!["pack", "publish", self.path().to_str().unwrap()]) + .output() + .await + .map(|_| ())?) + } + + /// Based on the loaded YAML, determine the pack type + pub(crate) fn get_pack_type(pack_yaml: &PackYaml) -> CodeQLPackType { + if let Some(library) = pack_yaml.library { + if library && pack_yaml.data_extensions.is_some() { + return CodeQLPackType::Models; + } else if library { + return CodeQLPackType::Library; + } + } else if pack_yaml.tests.is_some() { + return CodeQLPackType::Testing; + } else if pack_yaml.data_extensions.is_some() { + return CodeQLPackType::Models; + } + + CodeQLPackType::Queries + } +} + +impl Display for CodeQLPack { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(version) = self.version() { + write!(f, "{} ({}) - v{}", self.name(), self.pack_type(), version) + } else { + write!(f, "{} ({})", self.name(), self.pack_type(),) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_codeql_pack_display() { + let pack = CodeQLPack::new("codeql/javascript-queries@1.0.0"); + assert_eq!(pack.to_string(), "javascript-queries (Queries) - v1.0.0"); + } + + #[test] + fn test_codeql_pack_full_name() { + let pack = CodeQLPack::new("codeql/javascript-queries@1.0.0"); + assert_eq!(pack.full_name(), "codeql/javascript-queries@1.0.0"); + } + + #[test] + fn test_codeql_pack_namespace() { + let pack = CodeQLPack::new("codeql/javascript-queries"); + assert_eq!(pack.namespace(), "codeql"); + assert_eq!(pack.name(), "javascript-queries"); + + assert_eq!(pack.full_name(), "codeql/javascript-queries"); + } +} diff --git a/vendor/ghastoolkit/src/codeql/packs/packs.rs b/vendor/ghastoolkit/src/codeql/packs/packs.rs new file mode 100644 index 0000000..df80b1d --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/packs/packs.rs @@ -0,0 +1,79 @@ +//! CodeQL Packs module +use std::env::home_dir; +use std::path::PathBuf; + +use anyhow::Result; + +use crate::CodeQLPack; + +/// CodeQL Packs +#[derive(Debug, Clone, Default)] +pub struct CodeQLPacks { + packs: Vec, +} + +impl CodeQLPacks { + /// Get the number of packs + pub fn len(&self) -> usize { + self.packs.len() + } + /// Sort the packs by type (Library, Queries, Models, Testing) + pub fn sort(&mut self) { + self.packs.sort_by(|a, b| a.pack_type().cmp(&b.pack_type())); + } + /// Get the packs + pub fn packs(&self) -> &[CodeQLPack] { + &self.packs + } + /// Merge two CodeQL Packs + pub fn merge(&mut self, other: &mut Self) { + self.packs.append(&mut other.packs); + } + + /// Get the CodeQL Packages Path + pub(crate) fn codeql_packages_path() -> PathBuf { + let homedir = home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .canonicalize() + .unwrap_or_else(|_| PathBuf::from(".")); + homedir.join(".codeql").join("packages") + } + + /// Load CodeQL Packs from a directory. It will recursively search for `qlpack.yml` files. + pub fn load(path: impl Into) -> Result { + let path: PathBuf = path.into(); + let mut packs = Vec::new(); + + for entry in walkdir::WalkDir::new(&path) { + let entry = entry?; + + // Skip any subdirectories named `.codeql` + // TODO: Is this the best way to handle this? + if entry.path().to_str().unwrap().contains(".codeql") { + continue; + } + + if entry.file_name() == "qlpack.yml" { + let pack = CodeQLPack::new(entry.path().display().to_string()); + packs.push(pack); + } + } + + Ok(Self { packs }) + } +} + +impl IntoIterator for CodeQLPacks { + type Item = CodeQLPack; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.packs.into_iter() + } +} + +impl Extend for CodeQLPacks { + fn extend>(&mut self, iter: T) { + self.packs.extend(iter); + } +} diff --git a/vendor/ghastoolkit/src/codeql/version.rs b/vendor/ghastoolkit/src/codeql/version.rs new file mode 100644 index 0000000..1b1c11f --- /dev/null +++ b/vendor/ghastoolkit/src/codeql/version.rs @@ -0,0 +1,52 @@ +//! CodeQL Version / Release + +use std::fmt::Display; + +/// CodeQL Version +#[derive(Default, Clone, Debug, PartialEq, Eq)] +pub enum CodeQLVersion { + /// Latest CodeQL Version + #[default] + Latest, + /// Nightly version + Nightly, + /// Specific version of CodeQL + Version(String), +} + +impl Display for CodeQLVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CodeQLVersion::Latest => write!(f, "latest"), + CodeQLVersion::Nightly => write!(f, "nightly"), + CodeQLVersion::Version(v) => write!(f, "{v}"), + } + } +} + +impl From<&str> for CodeQLVersion { + fn from(value: &str) -> Self { + match value { + "latest" | "stable" => CodeQLVersion::Latest, + "nightly" | "unstable" => CodeQLVersion::Nightly, + v => CodeQLVersion::Version(v.to_string()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_codeql_version_from() { + assert_eq!(CodeQLVersion::from("latest"), CodeQLVersion::Latest); + assert_eq!(CodeQLVersion::from("stable"), CodeQLVersion::Latest); + assert_eq!(CodeQLVersion::from("nightly"), CodeQLVersion::Nightly); + assert_eq!(CodeQLVersion::from("unstable"), CodeQLVersion::Nightly); + assert_eq!( + CodeQLVersion::from("2.7.5"), + CodeQLVersion::Version("2.7.5".to_string()) + ); + } +} diff --git a/vendor/ghastoolkit/src/codescanning/api.rs b/vendor/ghastoolkit/src/codescanning/api.rs new file mode 100644 index 0000000..4f9be99 --- /dev/null +++ b/vendor/ghastoolkit/src/codescanning/api.rs @@ -0,0 +1,299 @@ +use std::path::PathBuf; + +use super::{CodeScanningHandler, models::ListCodeQLDatabase}; +use crate::{ + GHASError, Repository, + codeql::CodeQLLanguage, + codescanning::models::{CodeScanningAlert, CodeScanningAnalysis}, +}; +use log::debug; +use octocrab::{Octocrab, Page, Result as OctoResult}; + +impl<'octo> CodeScanningHandler<'octo> { + /// Create a new Code Scanning Handler instance + pub(crate) fn new(crab: &'octo Octocrab, repository: &'octo Repository) -> Self { + Self { crab, repository } + } + + /// Check if GitHub Code Scanning is enabled. This is done by checking + /// if the there is any analyses present for the repository. + pub async fn is_enabled(&self) -> bool { + match self.analyses().per_page(1).send().await { + Ok(_) => true, + Err(_) => { + debug!("Code scanning is not enabled for this repository"); + false + } + } + } + + /// Get a list of code scanning alerts for a repository + pub fn list(&self) -> ListCodeScanningAlerts { + ListCodeScanningAlerts::new(self) + } + + /// Get a single code scanning alert + pub async fn get(&self, number: u64) -> OctoResult { + let route = format!( + "/repos/{owner}/{repo}/code-scanning/alerts/{number}", + owner = self.repository.owner(), + repo = self.repository.name(), + number = number + ); + + self.crab.get(route, None::<&()>).await + } + + /// Get a list of code scanning analyses for a repository + pub fn analyses(&self) -> ListCodeScanningAnalyses { + ListCodeScanningAnalyses::new(self) + } + + /// List CodeQL databases + pub async fn list_codeql_databases(&self) -> OctoResult> { + let route = format!( + "/repos/{owner}/{repo}/code-scanning/codeql/databases", + owner = self.repository.owner(), + repo = self.repository.name() + ); + self.crab.get(route, None::<&()>).await + } + + /// Get a CodeQL database by language + pub async fn get_codeql_database(&self, language: String) -> OctoResult { + let route = format!( + "/repos/{owner}/{repo}/code-scanning/codeql/databases/{lang}", + owner = self.repository.owner(), + repo = self.repository.name(), + lang = language + ); + self.crab.get(route, None::<&()>).await + } + + /// Download a CodeQL database + /// + /// The Output is the root for where the database will be downloaded too. + /// + /// The output path will be something like this: + /// + /// ```text + /// output + /// └── owner + /// └── repo + /// └── {language} + /// ``` + /// + /// Links: + /// - https://docs.github.com/en/rest/code-scanning/code-scanning#get-a-codeql-database-for-a-repository + /// + pub async fn download_codeql_database( + &self, + language: impl Into, + output: impl Into, + ) -> Result { + let language = language.into(); + let output = output.into(); + // Create the path + let path = output + .join(self.repository.owner()) + .join(self.repository.name()) + .join(language.language()); + let dbpath = path.join("codeql-database.zip"); + + if path.exists() { + // Remove the path as there might be an existing database + std::fs::remove_dir_all(&path)?; + } + + std::fs::create_dir_all(&path)?; + log::info!("Downloading CodeQL database to {}", path.display()); + + // TODO: Download the database + let route = format!( + "/repos/{owner}/{repo}/code-scanning/codeql/databases/{lang}", + owner = self.repository.owner(), + repo = self.repository.name(), + lang = language.language() + ); + + // TODO: Swtich to useing the octocrab client + let client = reqwest::Client::new(); + let data = client + .get(route) + .header( + http::header::ACCEPT, + http::header::HeaderValue::from_str("application/zip")?, + ) + .header( + http::header::USER_AGENT, + http::header::HeaderValue::from_str("ghastoolkit")?, + ) + .send() + .await? + .bytes() + .await?; + + tokio::fs::write(&dbpath, data).await?; + + self.unzip_codeql_database(&dbpath, &path)?; + + Ok(path) + } + + /// Unzip the CodeQL database + fn unzip_codeql_database(&self, zip: &PathBuf, output: &PathBuf) -> Result<(), GHASError> { + log::debug!("Unzipping CodeQL database to {}", output.display()); + let file = std::fs::File::open(zip)?; + let mut archive = zip::ZipArchive::new(file)?; + archive.extract(output)?; + + Ok(()) + } +} + +/// List Code Scanning Analyses +#[derive(Debug, serde::Serialize)] +pub struct ListCodeScanningAlerts<'octo, 'b> { + #[serde(skip)] + handler: &'b CodeScanningHandler<'octo>, + + #[serde(skip_serializing_if = "Option::is_none")] + state: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tool_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + per_page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, +} + +impl<'octo, 'b> ListCodeScanningAlerts<'octo, 'b> { + pub(crate) fn new(handler: &'b CodeScanningHandler<'octo>) -> Self { + Self { + handler, + state: Some(String::from("open")), + tool_name: None, + // Default to 100 per page + per_page: Some(100), + // Default to page 1 + page: Some(1), + } + } + + /// Set the state of the code scanning alert + pub fn state(mut self, state: &str) -> Self { + self.state = Some(state.to_string()); + self + } + + /// Set the tool name of the code scanning alert + pub fn tool_name(mut self, tool_name: &str) -> Self { + self.tool_name = Some(tool_name.to_string()); + self + } + + /// Set the number of items per page + pub fn per_page(mut self, per_page: impl Into) -> Self { + self.per_page = Some(per_page.into()); + self + } + + /// Set the page number + pub fn page(mut self, page: impl Into) -> Self { + self.page = Some(page.into()); + self + } + + /// Send the request + pub async fn send(self) -> OctoResult> { + let route = format!( + "/repos/{owner}/{repo}/code-scanning/alerts", + owner = self.handler.repository.owner(), + repo = self.handler.repository.name() + ); + + self.handler.crab.get(route, Some(&self)).await + } +} + +/// List code scanning analyses +/// https://docs.github.com/en/rest/code-scanning/code-scanning?apiVersion=2022-11-28#list-code-scanning-analyses-for-a-repository +#[derive(Debug, serde::Serialize)] +pub struct ListCodeScanningAnalyses<'octo, 'b> { + #[serde(skip)] + handler: &'b CodeScanningHandler<'octo>, + + #[serde(skip_serializing_if = "Option::is_none")] + r#ref: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + tool_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + sarif_id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + per_page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, +} + +impl<'octo, 'b> ListCodeScanningAnalyses<'octo, 'b> { + pub(crate) fn new(handler: &'b CodeScanningHandler<'octo>) -> Self { + Self { + handler, + tool_name: None, + r#ref: None, + sarif_id: None, + // Default to 100 per page + per_page: Some(100), + // Default to page 1 + page: Some(1), + } + } + + /// Set the ref of the code scanning analysis + pub fn r#ref(mut self, r#ref: &str) -> Self { + self.r#ref = Some(r#ref.to_string()); + self + } + + /// Set the tool name of the code scanning analysis + pub fn tool_name(mut self, tool_name: &str) -> Self { + self.tool_name = Some(tool_name.to_string()); + self + } + + /// Set the sarif id of the code scanning analysis + pub fn sarif_id(mut self, sarif_id: &str) -> Self { + self.sarif_id = Some(sarif_id.to_string()); + self + } + + /// Set the number of items per page + pub fn per_page(mut self, per_page: impl Into) -> Self { + self.per_page = Some(per_page.into()); + self + } + + /// Set the page number + pub fn page(mut self, page: impl Into) -> Self { + self.page = Some(page.into()); + self + } + + /// Send the request + pub async fn send(self) -> OctoResult> { + let route = format!( + "/repos/{owner}/{repo}/code-scanning/analyses", + owner = self.handler.repository.owner(), + repo = self.handler.repository.name() + ); + + match self.handler.crab.get(route, Some(&self)).await { + Ok(response) => Ok(response), + Err(err) => Err(err), + } + } +} diff --git a/vendor/ghastoolkit/src/codescanning/configuration.rs b/vendor/ghastoolkit/src/codescanning/configuration.rs new file mode 100644 index 0000000..06b6d0c --- /dev/null +++ b/vendor/ghastoolkit/src/codescanning/configuration.rs @@ -0,0 +1,204 @@ +//! # Code Scanning Configuration + +use super::CodeScanningHandler; +use super::models::CodeScanningConfiguration; +use octocrab::Result as OctoResult; + +impl<'octo> CodeScanningHandler<'octo> { + /// Get the configuration for code scanning + pub async fn get_configuration(&self) -> OctoResult { + let route = format!( + "/repos/{owner}/{repo}/code-scanning/default-setup", + owner = self.repository.owner(), + repo = self.repository.name() + ); + self.crab.get(route, None::<&()>).await + } + + /// Update the configuration for code scanning using a builder pattern + /// + /// *Example:* + /// + /// ```no_run + /// # use anyhow::Result; + /// use ghastoolkit::prelude::*; + /// + /// # #[tokio::main] + /// # async fn main() -> Result<()> { + /// // Initialize a GitHub instance + /// let github = GitHub::init() + /// .owner("geekmasher") + /// .token("personal_access_token") + /// .build() + /// .expect("Failed to initialise GitHub instance"); + /// + /// // Create a repository instance + /// let repository = Repository::new("geekmasher", "ghastoolkit-rs"); + /// // Use the builder to create a code scanning configuration + /// github + /// .code_scanning(&repository) + /// .update_configuration() + /// .state("configured") + /// .language("rust") + /// .suite("default") + /// .threat_model("remote") + /// .send() + /// .await?; + /// + /// # Ok(()) + /// # } + /// ``` + pub fn update_configuration(&self) -> CodeScanningConfigurationBuilder { + CodeScanningConfigurationBuilder::new(self) + } + + /// Set the configuration for code scanning + /// + /// *Example:* + /// + /// ```no_run + /// # use anyhow::Result; + /// use ghastoolkit::prelude::*; + /// + /// # #[tokio::main] + /// # async fn main() -> Result<()> { + /// // Initialize a GitHub instance + /// let github = GitHub::init() + /// .owner("geekmasher") + /// .token("personal_access_token") + /// .build() + /// .expect("Failed to initialise GitHub instance"); + /// + /// // Create a repository instance + /// let repository = Repository::new("geekmasher", "ghastoolkit-rs"); + /// + /// // Create a code scanning configuration + /// let config = CodeScanningConfiguration { + /// state: String::from("configured"), + /// languages: vec![String::from("rust")], + /// ..Default::default() + /// }; + /// // Set the code scanning configuration + /// github + /// .code_scanning(&repository) + /// .set_configuration(&config) + /// .await?; + /// + /// # Ok(()) + /// # } + /// ``` + pub async fn set_configuration( + &self, + config: &CodeScanningConfiguration, + ) -> OctoResult { + let route = format!( + "/repos/{owner}/{repo}/code-scanning/default-setup", + owner = self.repository.owner(), + repo = self.repository.name() + ); + self.crab.patch(route, Some(&config)).await + } +} + +/// Code Scanning Configuration Builder +#[derive(Debug, Clone)] +pub struct CodeScanningConfigurationBuilder<'octo, 'handler> { + handler: &'handler CodeScanningHandler<'octo>, + config: CodeScanningConfiguration, +} + +impl<'octo, 'handler> CodeScanningConfigurationBuilder<'octo, 'handler> { + /// Create a new CodeScanningConfigurationBuilder + pub fn new(handler: &'handler CodeScanningHandler<'octo>) -> Self { + Self { + handler, + config: CodeScanningConfiguration::default(), + } + } + + /// Set the state of the code scanning configuration + /// + /// This can be "configured" or "non-configured". + pub fn state(mut self, state: impl Into) -> Self { + self.config.state = state.into(); + self + } + + /// Enable the code scanning configuration (equivalent to setting state to "configured") + pub fn enable(mut self) -> Self { + self.config.state = String::from("configured"); + self + } + + /// Disable the code scanning configuration (equivalent to setting state to "non-configured") + pub fn disable(mut self) -> Self { + self.config.state = String::from("non-configured"); + self + } + + /// Set a language for the code scanning configuration + pub fn language(mut self, language: impl Into) -> Self { + self.config.languages.push(language.into()); + self + } + + /// Set multiple languages for the code scanning configuration + pub fn languages(mut self, languages: Vec) -> Self { + self.config.languages = languages; + self + } + + /// Set the query suite for the code scanning configuration + /// + /// This can be either "default" or "extended". + pub fn suite(mut self, suite: impl Into) -> Self { + self.config.query_suite = suite.into(); + self + } + + /// Set the threat model for the code scanning configuration + /// + /// This can be either "remote" or "remote_and_local". + pub fn threat_model(mut self, threat_model: impl Into) -> Self { + self.config.threat_model = threat_model.into(); + self + } + + /// Set the thread model to "remote" + pub fn remote(mut self) -> Self { + self.config.threat_model = String::from("remote"); + self + } + + /// Set the thread model to "remote_and_local" + pub fn remote_and_local(mut self) -> Self { + self.config.threat_model = String::from("remote_and_local"); + self + } + + /// Set the GitHUb Action runner type for the code scanning configuration + /// + /// Can either be "standard" or "labeled" + pub fn runner_type(mut self, runner_type: impl Into) -> Self { + self.config.runner_type = Some(runner_type.into()); + self + } + + /// Set the GitHub Action runner label for the code scanning configuration + pub fn runner_label(mut self, runner_label: impl Into) -> Self { + self.config.runner_label = Some(runner_label.into()); + self.config.runner_type = Some(String::from("labeled")); + self + } + + /// Build the code scanning configuration + pub fn build(self) -> CodeScanningConfiguration { + self.config + } + + /// Send the request + pub async fn send(self) -> OctoResult<()> { + self.handler.set_configuration(&self.config).await?; + Ok(()) + } +} diff --git a/vendor/ghastoolkit/src/codescanning/mod.rs b/vendor/ghastoolkit/src/codescanning/mod.rs new file mode 100644 index 0000000..3f270ab --- /dev/null +++ b/vendor/ghastoolkit/src/codescanning/mod.rs @@ -0,0 +1,20 @@ +//! # Code Scanning module +//! +//! This is used to interact with GitHub's Code Scanning API + +use octocrab::Octocrab; + +use crate::Repository; + +/// GitHub Code Scanning API +pub mod api; +pub mod configuration; +/// GitHub Code Scanning Models +pub mod models; + +/// Code Scanning Handler +#[derive(Debug, Clone)] +pub struct CodeScanningHandler<'octo> { + pub(crate) crab: &'octo Octocrab, + pub(crate) repository: &'octo Repository, +} diff --git a/vendor/ghastoolkit/src/codescanning/models.rs b/vendor/ghastoolkit/src/codescanning/models.rs new file mode 100644 index 0000000..dfa93f8 --- /dev/null +++ b/vendor/ghastoolkit/src/codescanning/models.rs @@ -0,0 +1,173 @@ +use serde::{Deserialize, Serialize}; + +use crate::octokit::models::{Location, Message}; + +/// A code scanning alert. +/// https://docs.github.com/en/rest/code-scanning/code-scanning?apiVersion=2022-11-28#get-a-code-scanning-alert +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +pub struct CodeScanningAlert { + /// The ID of the alert. + pub number: i32, + /// Created at time. + pub created_at: String, + /// The URL of the alert. + pub url: String, + /// The HTML URL of the alert. + pub html_url: String, + /// The state of the alert. Can be "open", "fixed", etc. + pub state: String, + /// Fixed at time. + pub fixed_at: Option, + /// Dismissed by user. + pub dismissed_by: Option, + /// Dismissed at time. + pub dismissed_at: Option>, + /// Dismissed reason. + pub dismissed_reason: Option, + /// Dismissed comment. + pub dismissed_comment: Option, + /// The rule that triggered the alert. + pub rule: CodeScanningAlertRule, + /// The tool that generated the alert. + pub tool: CodeScanningAlertTool, + /// The most recent instance of the alert. + pub most_recent_instance: CodeScanningAlertInstance, + /// URL to the instances of the alert. + pub instances_url: String, +} + +/// A code scanning alert rule. +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +pub struct CodeScanningAlertRule { + /// The ID of the rule. + pub id: String, + /// The severity of the rule. + pub severity: String, + /// The tags of the rule. + pub tags: Vec, + /// The description of the rule. + pub description: String, + /// The name of the rule. + pub name: String, +} + +/// A code scanning alert instance. +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +pub struct CodeScanningAlertInstance { + /// The reference to the branch or tag the analysis was performed on. + pub r#ref: String, + /// Analysis key. + pub analysis_key: String, + /// Category. + pub category: String, + /// Environment. + pub environment: String, + /// The state of the alert instance. + pub state: String, + /// Commit SHA. + pub commit_sha: String, + /// Message. + pub message: Message, + /// Location. + pub location: Location, + /// Classifications. + pub classifications: Vec, +} + +/// A code scanning alert tool. +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +pub struct CodeScanningAlertTool { + /// The name of the tool. + pub name: String, + /// The guid of the tool. + pub guid: Option, + /// The version of the tool. + pub version: String, +} + +/// A code scanning alert dismissed by. +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +pub struct CodeScanningAlertDismissedBy {} + +/// A code scanning analysis. +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +pub struct CodeScanningAnalysis { + /// The reference to the branch or tag the analysis was performed on. + pub r#ref: String, + /// The commit SHA the analysis was performed on. + pub commit_sha: String, + /// The analysis key. + pub analysis_key: String, + /// The environment the analysis was performed in. + pub environment: String, + /// Error message. + pub error: Option, + /// Category. + pub category: String, + /// The time the analysis was created. + pub created_at: chrono::DateTime, + /// Number of alerts. + pub results_count: i32, + /// Rule count. + pub rules_count: i32, + /// ID of the analysis. + pub id: i32, + /// The URL of the analysis. + pub url: String, + /// SARIF ID. + pub sarif_id: String, + /// Code Scanning tool + pub tool: CodeScanningAlertTool, + /// Is the analysis deletable. + pub deletable: bool, + /// Warning message. + pub warning: Option, +} + +/// A CodeQL Database +#[derive(Debug, Clone, Default, Serialize, Deserialize, Hash, Eq, PartialEq)] +pub struct ListCodeQLDatabase { + /// ID + pub id: i32, + /// Name + pub name: String, + /// Language + pub language: String, + /// Content Type + pub content_type: String, + /// Size + pub size: i32, + /// Created At + pub created_at: chrono::DateTime, + /// Updated At + pub updated_at: chrono::DateTime, +} + +/// Code Scanning Configuration for Default Setup +#[derive(Debug, Clone, Default, Serialize, Deserialize, Hash, Eq, PartialEq)] +pub struct CodeScanningConfiguration { + /// State of the configuration + #[serde(skip_serializing_if = "String::is_empty")] + pub state: String, + /// Languages to scan + #[serde(skip_serializing_if = "Vec::is_empty")] + pub languages: Vec, + /// Query suite to use + #[serde(skip_serializing_if = "String::is_empty")] + pub query_suite: String, + /// Threat model + #[serde(skip_serializing_if = "String::is_empty")] + pub threat_model: String, + /// Action runner type + #[serde(skip_serializing_if = "Option::is_none")] + pub runner_type: Option, + /// Action runner label + #[serde(skip_serializing_if = "Option::is_none")] + pub runner_label: Option, + /// Last updated at time + #[serde(skip_serializing)] + pub updated_at: Option>, + /// Schedule for scanning + #[serde(skip_serializing)] + pub schedule: Option, +} diff --git a/vendor/ghastoolkit/src/errors.rs b/vendor/ghastoolkit/src/errors.rs new file mode 100644 index 0000000..05dfaab --- /dev/null +++ b/vendor/ghastoolkit/src/errors.rs @@ -0,0 +1,82 @@ +//! # GHASToolkit Errors +//! +//! This module contains all the errors that can be thrown by the library +use octocrab::Error as OctocrabError; +use regex::Error as RegexError; + +/// GitHub Advanced Security Toolkit Error +#[derive(thiserror::Error, Debug)] +pub enum GHASError { + /// Repository Reference Error + #[error("RepositoryReferenceError: {0}")] + RepositoryReferenceError(String), + + /// CodeQL Error + #[error("CodeQLError: {0}")] + CodeQLError(String), + + /// CodeQL Database Error + #[error("CodeQLDatabaseError: {0}")] + CodeQLDatabaseError(String), + + /// CodeQL Pack Error + #[error("CodeQLPackError: {0}")] + CodeQLPackError(String), + + /// Octocrab Error (octocrab::Error) + #[error("OctocrabError: {0}")] + OctocrabError(#[from] OctocrabError), + + /// GHActions Error + #[cfg(feature = "toolcache")] + #[error("GHActionsError: {0}")] + GHActionsError(#[from] ghactions::ActionsError), + + /// Regex Error (regex::Error) + #[error("RegexError: {0}")] + RegexError(#[from] RegexError), + + /// Io Error (std::io::Error) + #[error("IoError: {0}")] + IoError(#[from] std::io::Error), + + /// Serde Error (serde_json::Error) + #[error("SerdeError: {0}")] + SerdeError(#[from] serde_json::Error), + + /// Yaml Error (serde_yaml::Error) + #[error("YamlError: {0}")] + YamlError(#[from] serde_yaml::Error), + + /// Url Error (url::ParseError) + #[error("UrlError: {0}")] + UrlError(#[from] url::ParseError), + + /// Git Errors (git2::Error) + #[error("GitErrors: {0}")] + GitErrors(#[from] git2::Error), + + /// Zip Error (zip::result::ZipError) + #[error("ZipError: {0}")] + ZipError(#[from] zip::result::ZipError), + + /// Reqwest Error (reqwest::Error) + #[error("ReqwestError: {0}")] + ReqwestError(#[from] reqwest::Error), + + /// Http Error (http::Error) + #[error("HttpError: {0}")] + HttpInvalidHeader(#[from] http::header::InvalidHeaderValue), + + /// Walkdir Error (walkdir::Error) + #[error("WalkdirError: {0}")] + WalkdirError(#[from] walkdir::Error), + + /// Glob Error (glob::PatternError) + #[error("GlobError: {0}")] + GlobError(#[from] glob::PatternError), + + /// Unknown Error + #[error("UnknownError: {0}")] + UnknownError(String), +} diff --git a/vendor/ghastoolkit/src/lib.rs b/vendor/ghastoolkit/src/lib.rs new file mode 100644 index 0000000..354e622 --- /dev/null +++ b/vendor/ghastoolkit/src/lib.rs @@ -0,0 +1,49 @@ +#![forbid(unsafe_code)] +#![allow(dead_code)] +#![deny(missing_docs)] +#![doc = include_str!("../README.md")] + +pub mod codeql; +pub mod codescanning; +pub mod errors; +pub mod octokit; +pub mod secretscanning; +pub mod supplychain; +pub mod utils; + +pub use errors::GHASError; + +pub use octokit::github::GitHub; +pub use octokit::repository::Repository; + +// CodeQL +pub use codeql::extractors::{BuildMode, CodeQLExtractor}; +pub use codeql::packs::{CodeQLPack, CodeQLPackType, CodeQLPacks}; +pub use codeql::{CodeQL, CodeQLDatabase, CodeQLDatabases}; + +// Supply Chain +pub use supplychain::{Dependencies, Dependency, License, Licenses}; + +// Utilities +pub use utils::sarif::Sarif; + +#[doc(hidden)] +#[allow(unused_imports, missing_docs)] +pub mod prelude { + pub use crate::errors::GHASError; + pub use crate::octokit::github::GitHub; + pub use crate::octokit::repository::Repository; + + // CodeQL + pub use crate::codeql::extractors::{BuildMode, CodeQLExtractor}; + pub use crate::codeql::packs::{CodeQLPack, CodeQLPackType, CodeQLPacks}; + pub use crate::codeql::{CodeQL, CodeQLDatabase, CodeQLDatabases}; + + // Code Scanning + pub use crate::codescanning::models::{ + CodeScanningAlert, CodeScanningAnalysis, CodeScanningConfiguration, + }; + + // Supply Chain + pub use crate::supplychain::{Dependencies, Dependency, License, Licenses}; +} diff --git a/vendor/ghastoolkit/src/octokit/github.rs b/vendor/ghastoolkit/src/octokit/github.rs new file mode 100644 index 0000000..19b1a12 --- /dev/null +++ b/vendor/ghastoolkit/src/octokit/github.rs @@ -0,0 +1,430 @@ +use std::{fmt::Display, path::PathBuf}; + +use git2::Repository as GitRepository; +use log::debug; +use octocrab::{Octocrab, Result as OctoResult}; +use url::Url; + +use crate::{ + GHASError, Repository, codescanning::CodeScanningHandler, octokit::models::GitHubLanguages, + secretscanning::api::SecretScanningHandler, +}; + +/// GitHub instance +#[derive(Debug, Clone)] +pub struct GitHub { + /// Octocrab instance + octocrab: Octocrab, + + /// Owner of the repository (organization or user) + owner: Option, + /// Enterprise account name (if applicable) + enterprise: Option, + + /// GitHub token (personal access token or GitHub App token) + token: Option, + + /// GitHub instance (e.g. https://github.com or enterprise server instance) + instance: Url, + /// REST API endpoint + api_rest: Url, + + /// If an enterprise server instance is being used + enterprise_server: bool, + + /// If the token is for a GitHub App + github_app: bool, +} + +impl GitHub { + /// Initialize a new GitHub instance with default values + pub fn new() -> Self { + GitHub::default() + } + + /// Initialize a new GitHub instance with a builder pattern + /// + /// # Example + /// ```rust + /// use ghastoolkit::GitHub; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let github = GitHub::init() + /// .owner("geekmasher") + /// .token("personal_access_token") + /// .build() + /// .expect("Failed to initialise GitHub instance"); + /// # } + /// ``` + pub fn init() -> GitHubBuilder { + GitHubBuilder::default() + } + + /// Check if the GitHub instance is for Enterprise Server or Cloud. + /// This is done based off the URL provided. + pub fn is_enterprise_server(&self) -> bool { + self.enterprise_server + } + + /// Get the GitHub instance URL as a String + pub fn instance(&self) -> String { + self.instance.to_string() + } + + /// Get the GitHub Token + pub fn token(&self) -> Option<&String> { + self.token.as_ref() + } + + /// Get the Base URL for the GitHub REST API + pub(crate) fn base(&self) -> Url { + self.api_rest.clone() + } + + /// Get the URL used for clong a repository. + fn clone_repository_url(&self, repo: &Repository) -> Result { + if self.github_app { + // GitHub Apps require a different URL + Ok(format!( + "{}://x-access-token:{}@{}/{}/{}.git", + self.instance.scheme(), + self.token.clone().expect("Failed to get token"), + self.instance.host().expect("Failed to get host"), + repo.owner(), + repo.name() + )) + } else if let Some(token) = &self.token { + Ok(format!( + "{}://{}@{}/{}/{}.git", + self.instance.scheme(), + token, + self.instance.host().expect("Failed to get host"), + repo.owner(), + repo.name() + )) + } else { + // No token + Ok(format!( + "{}://{}/{}/{}.git", + self.instance.scheme(), + self.instance.host().expect("Failed to get host"), + repo.owner(), + repo.name() + )) + } + } + + /// Get the pre-build instance of Octocrab. + /// This has automatically configured the base URI, PAT, and other settings. + /// + /// # Example + /// ```no_run + /// # use anyhow::Result; + /// use ghastoolkit::GitHub; + /// + /// # #[tokio::main] + /// # async fn main() -> Result<()> { + /// let github = GitHub::init() + /// .owner("geekmasher") + /// .token("personal_access_token") + /// .build() + /// .expect("Failed to initialise GitHub instance"); + /// + /// let octocrab = github.octocrab(); + /// + /// let issues = octocrab.issues("geekmasher", "ghastoolkit-rs") + /// .list() + /// .state(octocrab::params::State::Open) + /// .send() + /// .await?; + /// + /// # Ok(()) + /// # } + /// ``` + pub fn octocrab(&self) -> &octocrab::Octocrab { + &self.octocrab + } + + /// Get Secret Scanning Handler based on the Repository + #[allow(elided_named_lifetimes)] + pub fn secret_scanning<'a>(&'a self, repo: &'a Repository) -> SecretScanningHandler { + SecretScanningHandler::new(self.octocrab(), repo) + } + + /// Get Code Scanning Handler based on the Repository provided. + #[allow(elided_named_lifetimes)] + pub fn code_scanning<'a>(&'a self, repo: &'a Repository) -> CodeScanningHandler { + CodeScanningHandler::new(self.octocrab(), repo) + } + + /// Get Repository languages from GitHub + pub async fn list_languages(&self, repo: &Repository) -> OctoResult { + let route = format!("/repos/{}/{}/languages", repo.owner(), repo.name()); + self.octocrab.get(route, None::<&()>).await + } + + /// Clone a GitHub Repository to a local path + pub fn clone_repository( + &self, + repo: &mut Repository, + path: &String, + ) -> Result { + let url = self.clone_repository_url(repo)?; + match GitRepository::clone(url.as_str(), path.as_str()) { + Ok(gitrepo) => { + repo.set_root(PathBuf::from(path)); + Ok(gitrepo) + } + Err(e) => Err(GHASError::from(e)), + } + } +} + +impl Display for GitHub { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "GitHub(instance: {:?}, owner: '{:?}', enterprise: {:?})", + self.instance.to_string(), + self.owner, + self.enterprise, + ) + } +} + +impl Default for GitHub { + /// GitHub defaults to using Environment Variables for the GitHub instance and token. + fn default() -> Self { + let instance = match std::env::var("GITHUB_INSTANCE") { + Ok(val) => Url::parse(val.as_str()).expect("Failed to parse GitHub instance URL"), + Err(_) => { + Url::parse("https://github.com").expect("Failed to parse GitHub instance URL") + } + }; + // TODO(geekmasher): REST API + let token = match std::env::var("GITHUB_TOKEN") { + Ok(val) => Some(val), + Err(_) => None, + }; + + Self { + octocrab: octocrab::Octocrab::default(), + owner: None, + enterprise: None, + token, + instance, + api_rest: Url::parse("https://api.github.com") + .expect("Failed to parse GitHub REST API URL"), + enterprise_server: false, + github_app: false, + } + } +} + +/// GitHub Builder +#[derive(Debug, Clone)] +pub struct GitHubBuilder { + owner: Option, + enterprise: Option, + token: Option, + instance: Url, + rest_api: Url, + enterprise_server: bool, + github_app: bool, +} + +impl GitHubBuilder { + /// Set the Instance URL for the GitHub Enterprise Server. + /// + /// # Example + /// ```rust + /// use ghastoolkit::GitHub; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let github = GitHub::init() + /// .instance("https://github.geekmasher.dev/") + /// .build() + /// .expect("Failed to initialise GitHub instance"); + /// + /// # assert_eq!(github.instance(), "https://github.geekmasher.dev/"); + /// # } + /// ``` + pub fn instance(&mut self, instance: &str) -> &mut Self { + self.instance = Url::parse(instance).expect("Failed to parse instance URL"); + + // GitHub Cloud + if self.instance.host_str() == Some("github.com") { + self.rest_api = + Url::parse("https://api.github.com").expect("Failed to parse REST API URL"); + self.enterprise_server = false; + } else { + // GitHub Enterprise Server endpoint + self.rest_api = Url::parse(format!("{}/api/v3", instance).as_str()) + .expect("Failed to parse REST API URL"); + self.enterprise_server = true; + } + + self + } + /// Set the Token used to authenticate with GitHub. + /// + /// # Example + /// ```rust + /// use ghastoolkit::GitHub; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let github = GitHub::init() + /// .token("personal_access_token") + /// .build() + /// .expect("Failed to initialise GitHub instance"); + /// + /// # assert_eq!(github.token(), Some(&String::from("personal_access_token"))); + /// # } + /// ``` + pub fn token(&mut self, token: &str) -> &mut Self { + if !token.is_empty() { + log::debug!("Setting token"); + self.token = Some(token.to_string()); + } + self + } + + /// Set the Owner (Username or Organization). + pub fn owner(&mut self, owner: &str) -> &mut Self { + if !owner.is_empty() { + self.owner = Some(owner.to_string()); + } + self + } + + /// Set the Enterprise Account name. + pub fn enterprise(&mut self, enterprise: &str) -> &mut Self { + if !enterprise.is_empty() { + log::debug!("Setting enterprise"); + self.enterprise = Some(enterprise.to_string()); + } + self + } + + /// Set the GitHub App flag. This is mainly used for changing the rate limits and other + /// settings. + pub fn github_app(&mut self, github_app: bool) -> &mut Self { + self.github_app = github_app; + self + } + + /// Build the GitHub instance with the provided settings. + /// + /// # Example + /// ```rust + /// use ghastoolkit::GitHub; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let github = GitHub::init() + /// .instance("https://github.geekmasher.dev/") + /// .owner("geekmasher") + /// .token("personal_access_token") + /// .build() + /// .expect("Failed to initialise GitHub instance"); + /// # } + /// ``` + pub fn build(&self) -> Result { + let token = match self.token.clone() { + Some(token) => Some(token), + None => std::env::var("GITHUB_TOKEN").ok(), + }; + + let mut builder = octocrab::Octocrab::builder() + .base_uri(self.rest_api.to_string().as_str())? + .add_header( + http::header::USER_AGENT, + format!("ghastoolkit/{}", env!("CARGO_PKG_VERSION")), + ) + .add_header( + http::HeaderName::from_static("x-github-api-version"), + "2022-11-28".to_string(), + ) + .add_header( + http::header::ACCEPT, + "application/vnd.github.v3+json".to_string(), + ); + + debug!("Setting base URI to: {}", self.rest_api); + + if let Some(token) = &self.token { + debug!("Setting personal token"); + builder = builder.personal_token(token.clone()); + } else { + debug!("No credential provided"); + } + + let client = builder.build()?; + log::debug!("Octocrab client: {:?}", client); + + Ok(GitHub { + octocrab: client, + owner: self.owner.clone(), + enterprise: self.enterprise.clone(), + token, + instance: self.instance.clone(), + api_rest: self.rest_api.clone(), + enterprise_server: self.enterprise_server, + github_app: self.github_app, + }) + } +} + +impl Default for GitHubBuilder { + fn default() -> Self { + Self { + owner: None, + enterprise: None, + token: None, + instance: Url::parse("https://github.com") + .expect("Failed to parse GitHub instance URL"), + rest_api: Url::parse("https://api.github.com") + .expect("Failed to parse GitHub REST API URL"), + enterprise_server: false, + github_app: false, + } + } +} +#[cfg(test)] +mod test { + use super::*; + + #[tokio::test] + async fn test_github_builder() { + let gh = GitHub::init() + .instance("https://github.com") + .token("token") + .owner("geekmasher") + .build() + .expect("Failed to build GitHub instance"); + + assert_eq!(gh.instance, Url::parse("https://github.com").unwrap()); + assert_eq!(gh.token, Some("token".to_string())); + assert_eq!(gh.owner, Some("geekmasher".to_string())); + } + + #[tokio::test] + async fn test_repo_clone_url() { + let gh = GitHub::init() + .instance("https://github.com") + .token("token") + .owner("geekmasher") + .build() + .expect("Failed to build GitHub instance"); + let repo = Repository::try_from("geekmasher/ghastoolkit@main") + .expect("Failed to parse repository"); + + let url = gh + .clone_repository_url(&repo) + .expect("Failed to get clone URL"); + assert_eq!(url, "https://token@github.com/geekmasher/ghastoolkit.git"); + } +} diff --git a/vendor/ghastoolkit/src/octokit/mod.rs b/vendor/ghastoolkit/src/octokit/mod.rs new file mode 100644 index 0000000..42d3f19 --- /dev/null +++ b/vendor/ghastoolkit/src/octokit/mod.rs @@ -0,0 +1,8 @@ +//! Octokit is a GitHub API client for Rust. + +/// GitHub +pub mod github; +/// GitHub Models +pub mod models; +/// GitHub Repository +pub mod repository; diff --git a/vendor/ghastoolkit/src/octokit/models.rs b/vendor/ghastoolkit/src/octokit/models.rs new file mode 100644 index 0000000..e7ab719 --- /dev/null +++ b/vendor/ghastoolkit/src/octokit/models.rs @@ -0,0 +1,28 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +/// GitHub Message block +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +pub struct Message { + /// The message text + pub text: String, +} + +/// GItHub Source Location +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +pub struct Location { + /// Path + pub path: String, + /// Start Line + pub start_line: u32, + /// End Line + pub end_line: u32, + /// Start Column + pub start_column: u32, + /// End Column + pub end_column: u32, +} + +/// GitHub Languages +pub type GitHubLanguages = HashMap; diff --git a/vendor/ghastoolkit/src/octokit/repository.rs b/vendor/ghastoolkit/src/octokit/repository.rs new file mode 100644 index 0000000..444b724 --- /dev/null +++ b/vendor/ghastoolkit/src/octokit/repository.rs @@ -0,0 +1,307 @@ +//! # Repository +//! +//! **Example:** +//! +//! ```rust +//! use ghastoolkit::Repository; +//! +//! let repo = Repository::new("geekmasher".to_string(), "ghastoolkit-rs".to_string()); +//! +//! ``` +use git2::Repository as GitRepository; +use std::{fmt::Display, path::PathBuf}; + +use log::debug; +use regex::Regex; + +use crate::errors::GHASError; + +/// GitHub Repository +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct Repository { + /// Owner of the repository (organization or user) + owner: String, + /// Name of the repository + name: String, + /// Full reference (e.g. refs/heads/main) + reference: Option, + /// Branch name (e.g. main) + branch: Option, + + /// Path to a file or directory relative to the repository root + path: PathBuf, + + /// Repository root path + root: PathBuf, +} + +impl Repository { + /// Create a new Repository instance with owner (organization or user) and repo name + /// + /// **Example:** + /// + /// ```rust + /// use ghastoolkit::Repository; + /// + /// let repo = Repository::new("geekmasher", "ghastoolkit-rs"); + /// + /// # assert_eq!(repo.owner(), "geekmasher"); + /// # assert_eq!(repo.name(), "ghastoolkit-rs"); + /// ``` + pub fn new(owner: impl Into, repo: impl Into) -> Self { + Self { + owner: owner.into(), + name: repo.into(), + ..Default::default() + } + } + + /// Initialize a new Repository instance with a builder pattern + /// + /// # Example + /// ```rust + /// use ghastoolkit::Repository; + /// + /// let repo = Repository::init() + /// .owner("geekmasher") + /// .name("ghastoolkit-rs") + /// .build(); + /// println!("{:?}", repo); + /// ``` + pub fn init() -> RepositoryBuilder { + RepositoryBuilder::default() + } + + /// Get the Repository owner + pub fn owner(&self) -> &str { + &self.owner + } + + /// Get the Repository name + pub fn name(&self) -> &str { + &self.name + } + /// Get the Repository reference + pub fn reference(&self) -> Option<&str> { + self.reference.as_deref() + } + + /// Get the Repository branch + pub fn branch(&self) -> Option<&str> { + self.branch.as_deref() + } + + /// Get file or directory relative to the repository root + pub fn path(&self) -> &PathBuf { + &self.path + } + + /// Get full path to file or directory relative to the repository root + pub fn fullpath(&self) -> PathBuf { + self.root.join(&self.path) + } + + /// Get root path of the repository + pub fn root(&self) -> &PathBuf { + &self.root + } + + /// Set the Repository root path + pub fn set_root(&mut self, root: PathBuf) { + self.root = root; + } + + /// Get the Git SHA of the repository + pub fn gitsha(&self) -> Option { + if self.root.exists() { + // PathBuf to str + if let Some(path) = self.path.to_str() { + match GitRepository::open(path) { + Ok(repo) => { + debug!("Repository found: {:?}", repo.path()); + // TODO(geekmasher): Handle errors + return Some(repo.head().unwrap().target().unwrap().to_string()); + } + Err(e) => { + debug!("Failed to open repository: {:?}", e); + return None; + } + } + } + debug!("Failed to convert PathBuf to str"); + return None; + } + debug!("Repository root does not exist"); + None + } + + /// Parse and return a Repository instance from a repository reference + /// + /// # Samples: + /// + /// - `geekmasher/ghastoolkit-rs` + /// - `geekmasher/ghastoolkit-rs@main` + /// - `geekmasher/ghastoolkit-rs:src/main.rs` + /// - `geekmasher/ghastoolkit-rs:src/main.rs@main` + /// + /// # Example + /// + /// ```rust + /// use ghastoolkit::Repository; + /// + /// let repo = Repository::parse("geekmasher/ghastoolkit-rs") + /// .expect("Failed to parse repository reference"); + /// + /// println!("{}", repo); + /// ``` + /// + pub fn parse(reporef: &str) -> Result { + let mut repository = Repository::default(); + + // regex match check + let re = Regex::new( + r"^[a-zA-Z0-9-_\.]+/[a-zA-Z0-9-_\.]+((:|/)[a-zA-Z0-9-_/\.]+)?(@[a-zA-Z0-9-_/]+)?$", + )?; + + re.is_match(reporef).then(|| { + let mut current = reporef.to_string(); + // parse the repository reference + match current.split_once('@') { + Some((repo, branch)) => { + repository.branch = Some(branch.to_string()); + repository.reference = Some(format!("refs/heads/{}", branch)); + + current = repo.to_string(); + } + _ => { + debug!("No reference found in repository reference"); + } + } + // TODO(geekmasher): Support for `:` in the repository reference + + let blocks = current.split('/').collect::>(); + for (i, block) in blocks.iter().enumerate() { + match i { + 0 => repository.owner = block.to_string(), + 1 => repository.name = block.to_string(), + _ => repository.path.push(block), + } + } + }); + + Ok(repository) + } +} + +impl Display for Repository { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(branch) = &self.branch { + write!(f, "{}/{}@{}", self.owner, self.name, branch) + } else { + write!(f, "{}/{}", self.owner, self.name) + } + } +} + +impl TryFrom<&str> for Repository { + type Error = GHASError; + + fn try_from(reporef: &str) -> Result { + Repository::parse(reporef) + } +} + +/// Repository Builder pattern +#[derive(Debug, Default, Clone)] +pub struct RepositoryBuilder { + owner: String, + name: String, + reference: Option, + branch: Option, + path: PathBuf, + root: PathBuf, +} + +impl RepositoryBuilder { + /// Set the Repository owner + pub fn owner(&mut self, owner: &str) -> &mut Self { + self.owner = owner.to_string(); + self + } + + /// Set the Repository name + pub fn name(&mut self, name: &str) -> &mut Self { + self.name = name.to_string(); + self + } + + /// Set the Repository owner/name + pub fn repo(&mut self, repo: &str) -> &mut Self { + if let Some((owner, name)) = repo.split_once('/') { + self.owner = owner.to_string(); + self.name = name.to_string(); + } + self + } + + /// Set the Repository reference + pub fn reference(&mut self, reference: &str) -> &mut Self { + self.reference = Some(reference.to_string()); + if let Some((_, branch)) = reference.split_once("heads/") { + self.branch = Some(branch.to_string()); + } + self + } + + /// Set the Repository branch + pub fn branch(&mut self, branch: &str) -> &mut Self { + if !branch.is_empty() { + self.branch = Some(branch.to_string()); + self.reference = Some(format!("refs/heads/{}", branch)); + } + self + } + + /// Set the Repository path + pub fn path(&mut self, path: &str) -> &mut Self { + self.path = PathBuf::from(path); + self + } + + /// Set the Repository root source path + pub fn root(&mut self, root: &str) -> &mut Self { + self.root = PathBuf::from(root); + self + } + + /// Build the Repository + pub fn build(&self) -> Result { + Ok(Repository { + owner: self.owner.clone(), + name: self.name.clone(), + reference: self.reference.clone(), + branch: self.branch.clone(), + path: self.path.clone(), + root: self.root.clone(), + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_try_from() { + let repository = Repository::try_from("owner/repo@main").unwrap(); + assert_eq!(repository.owner, "owner"); + assert_eq!(repository.name, "repo"); + assert_eq!(repository.branch, Some("main".to_string())); + + let repository = Repository::try_from("owner/repo/path/to/file@main").unwrap(); + assert_eq!(repository.owner, "owner"); + assert_eq!(repository.name, "repo"); + assert_eq!(repository.path, PathBuf::from("path/to/file")); + assert_eq!(repository.branch, Some("main".to_string())); + } +} diff --git a/vendor/ghastoolkit/src/secretscanning/api.rs b/vendor/ghastoolkit/src/secretscanning/api.rs new file mode 100644 index 0000000..8aa8424 --- /dev/null +++ b/vendor/ghastoolkit/src/secretscanning/api.rs @@ -0,0 +1,128 @@ +//! # Secret Scanning Alert + +use octocrab::{Octocrab, Page, Result as OctoResult}; + +use crate::Repository; + +use super::secretalerts::{SecretScanningAlert, SecretScanningSort}; + +/// Secret Scanning Handler +#[derive(Debug, Clone)] +pub struct SecretScanningHandler<'octo> { + crab: &'octo Octocrab, + repository: &'octo Repository, +} + +impl<'octo> SecretScanningHandler<'octo> { + /// Create a new Code Scanning Handler instance + pub(crate) fn new(crab: &'octo Octocrab, repository: &'octo Repository) -> Self { + Self { crab, repository } + } + + /// Get a list of code scanning alerts for a repository + pub fn list(&self) -> ListSecretScanningAlerts { + ListSecretScanningAlerts::new(self) + } + + /// Get a single code scanning alert + pub async fn get(&self, number: u64) -> OctoResult { + let route = format!( + "/repos/{owner}/{repo}/secret-scanning/alerts/{number}", + owner = self.repository.owner(), + repo = self.repository.name(), + number = number + ); + + self.crab.get(route, None::<&()>).await + } +} + +/// List Secret Scanning Alerts +#[derive(Debug, serde::Serialize)] +pub struct ListSecretScanningAlerts<'octo, 'b> { + #[serde(skip)] + handler: &'b SecretScanningHandler<'octo>, + + #[serde(skip_serializing_if = "Option::is_none")] + state: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + secret_type: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + sort: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + validity: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + per_page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, +} + +impl<'octo, 'b> ListSecretScanningAlerts<'octo, 'b> { + pub(crate) fn new(handler: &'b SecretScanningHandler<'octo>) -> Self { + Self { + handler, + state: Some(String::from("open")), + secret_type: None, + sort: None, + validity: None, + // Default to 100 per page + per_page: Some(25), + // Default to page 1 + page: Some(1), + } + } + + /// Set the state of the code scanning alert + pub fn state(mut self, state: impl Into) -> Self { + let state = state.into(); + if !state.is_empty() { + self.state = Some(state); + } + self + } + + /// Set the Secret Type + pub fn secret_type(mut self, stype: impl Into) -> Self { + self.secret_type = Some(stype.into()); + self + } + + /// Sort + pub fn sort(mut self, sort: impl Into) -> Self { + self.sort = Some(sort.into()); + self + } + + /// Validity + pub fn validity(mut self, validity: impl Into) -> Self { + self.validity = Some(validity.into()); + self + } + + /// Set the number of items per page + pub fn per_page(mut self, per_page: impl Into) -> Self { + self.per_page = Some(per_page.into()); + self + } + + /// Set the page number + pub fn page(mut self, page: impl Into) -> Self { + self.page = Some(page.into()); + self + } + + /// Send the request + pub async fn send(self) -> OctoResult> { + let route = format!( + "/repos/{owner}/{repo}/secret-scanning/alerts", + owner = self.handler.repository.owner(), + repo = self.handler.repository.name() + ); + + self.handler.crab.get(route, Some(&self)).await + } +} diff --git a/vendor/ghastoolkit/src/secretscanning/mod.rs b/vendor/ghastoolkit/src/secretscanning/mod.rs new file mode 100644 index 0000000..59c7622 --- /dev/null +++ b/vendor/ghastoolkit/src/secretscanning/mod.rs @@ -0,0 +1,30 @@ +//! # Secret Scanning +//! +//! # Example +//! +//! ```no_run +//! # use anyhow::Result; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<()> { +//! +//! let github = ghastoolkit::GitHub::init() +//! .owner("geekmasher") +//! .token("personal_access_token") +//! .build() +//! .expect("Failed to initialise GitHub instance"); +//! +//! let repo = ghastoolkit::Repository::new("geekmasher", "ghastoolkit-rs"); +//! +//! let alerts = github +//! .secret_scanning(&repo) +//! .list() +//! .send() +//! .await?; +//! +//! # Ok(()) +//! # } +//! ``` + +pub mod api; +pub mod secretalerts; diff --git a/vendor/ghastoolkit/src/secretscanning/secretalerts.rs b/vendor/ghastoolkit/src/secretscanning/secretalerts.rs new file mode 100644 index 0000000..f9e038f --- /dev/null +++ b/vendor/ghastoolkit/src/secretscanning/secretalerts.rs @@ -0,0 +1,130 @@ +//! # Secret Scanning Alerts +use std::fmt::Display; + +use octocrab::models::SimpleUser; +use serde::{Deserialize, Serialize}; +use url::Url; + +/// Secret Scanning Alert Status +#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +#[serde(rename_all = "snake_case")] +pub enum SecretScanningAlertStatus { + /// Open Alert + #[serde(rename = "open")] + Open, + /// Resolved Alert + #[serde(rename = "resolved")] + Resolved, +} + +impl Display for SecretScanningAlertStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SecretScanningAlertStatus::Open => write!(f, "Open"), + SecretScanningAlertStatus::Resolved => write!(f, "Resolved"), + } + } +} + +/// Secret Scanning Alert Resolution +#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +#[serde(rename_all = "snake_case")] +pub enum SecretScanningAlertResolution { + /// Resolved as a False Positive + #[serde(rename = "false_positive")] + FalsePositive, + /// Wont Fix + #[serde(rename = "wont_fix")] + WontFix, + /// Revoked + #[serde(rename = "revoked")] + Revoked, + /// Pattern Edited + #[serde(rename = "pattern_edited")] + PatternEdited, + /// Pattern Deleted + #[serde(rename = "pattern_deleted")] + PatternDeleted, + /// Used in Tests + #[serde(rename = "used_in_tests")] + UsedInTests, +} + +/// Secret Scanning Validity +#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +#[serde(rename_all = "snake_case")] +pub enum SecretScanningAlertValidity { + /// Active + #[serde(rename = "active")] + Active, + /// Inactive + #[serde(rename = "inactive")] + Inactive, + /// Unknown + #[serde(rename = "unknown")] + Unknown, +} + +/// Secret Scanning Validity +#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +#[serde(rename_all = "snake_case")] +pub enum SecretScanningSort { + /// Active + #[serde(rename = "created")] + Created, + /// Inactive + #[serde(rename = "updated")] + Updated, +} + +/// A Secret Scanning Alert +/// +/// https://docs.github.com/en/rest/secret-scanning/secret-scanning?apiVersion=2022-11-28 +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +pub struct SecretScanningAlert { + /// The ID of the alert + pub number: u64, + /// Creation time of the alert + pub created_at: chrono::DateTime, + + /// State of the alert + pub state: SecretScanningAlertStatus, + + /// Secret Scanning type + pub secret_type: String, + /// Secret Scanning type display name + pub secret_type_display_name: String, + + /// Secret Value + pub secret: String, + + /// Alert Resolution + pub resolved: Option, + /// When the alert was resolved + pub resolved_at: Option>, + /// Who resolved the alert + pub resolved_by: Option, + /// User resolution comment + pub resolution_comment: Option, + + /// Is Push Protection enabled + pub push_protection_bypassed: Option, + /// Who bypassed push protection + pub push_protection_bypassed_by: Option, + /// When it was bypassed + pub push_protection_bypassed_at: Option>, + + /// Validity check + pub validity: Option, + + /// URL + pub url: Url, + /// HTML + pub html_url: Url, + /// Locations + pub locations_url: Url, +} diff --git a/vendor/ghastoolkit/src/supplychain/dependencies.rs b/vendor/ghastoolkit/src/supplychain/dependencies.rs new file mode 100644 index 0000000..86f8576 --- /dev/null +++ b/vendor/ghastoolkit/src/supplychain/dependencies.rs @@ -0,0 +1,147 @@ +use crate::{ + Dependency, + supplychain::{License, Licenses}, +}; + +/// List of Dependencies +#[derive(Debug, Clone, Default)] +pub struct Dependencies { + dependencies: Vec, +} + +impl Iterator for Dependencies { + type Item = Dependency; + + fn next(&mut self) -> Option { + self.dependencies.pop() + } +} + +impl Dependencies { + /// Create a new list of dependencies + /// + /// # Example + /// + /// ```rust + /// use ghastoolkit::{Dependency, Dependencies}; + /// + /// let mut dependencies = Dependencies::new(); + /// + /// dependencies.push(Dependency::from("pkg:cargo/ghastoolkit-rs@0.2.0")); + /// + /// for dependency in dependencies { + /// println!("{}", dependency); + /// // Do something with the dependency + /// } + /// ``` + pub fn new() -> Self { + Self { + dependencies: Vec::new(), + } + } + + /// Push a new dependency to the list + pub fn push(&mut self, dependency: Dependency) { + self.dependencies.push(dependency); + } + + /// Extend the list of dependencies with a new list of dependencies + pub fn extend(&mut self, dependencies: Vec) { + self.dependencies.extend(dependencies); + } + + /// Get the length of the list of dependencies + pub fn len(&self) -> usize { + self.dependencies.len() + } + + /// Check if the list of dependencies is empty + pub fn is_empty(&self) -> bool { + self.dependencies.is_empty() + } + + /// Check if the list contains a particular Dependency + pub fn contains(&self, dependency: &Dependency) -> bool { + self.dependencies.contains(dependency) + } + + /// Find a dependency by name + pub fn find_by_name(&self, name: &str) -> Option { + self.dependencies.iter().find(|d| d.name == name).cloned() + } + + /// Find a list of dependencies by names + pub fn find_by_names(&self, names: &[&str]) -> Vec { + self.dependencies + .iter() + .filter(|d| names.contains(&d.name.as_str())) + .cloned() + .collect() + } + + /// Find a list of dependencies by license + pub fn find_by_license(&self, license: &License) -> Vec { + // TODO(geekmasher): support for wildcard licenses + self.dependencies + .iter() + .filter(|d| d.licenses.contains(license)) + .cloned() + .collect() + } + + /// Find a list of dependencies by licenses + pub fn find_by_licenses(&self, licenses: &Licenses) -> Vec { + // TODO(geekmasher): the clone here is not great, but it's a quick fix for now + self.dependencies + .iter() + .filter(|d| { + d.licenses + .clone() + .into_iter() + .any(|l| licenses.contains(&l)) + }) + .cloned() + .collect() + } +} + +#[cfg(test)] +mod tests { + use crate::{Dependencies, Dependency, supplychain::License}; + + #[test] + fn test_find_by_name() { + let mut deps = Dependencies::new(); + deps.extend(vec![ + Dependency::from("pkg:cargo/ghastoolkit-rs@0.2.0"), + Dependency::from("pkg:pip/ghastoolkit@0.12.0"), + ]); + + assert_eq!(deps.len(), 2); + + let dep = deps + .find_by_name("ghastoolkit-rs") + .expect("Failed to find dependency by name"); + + assert_eq!(dep.name, "ghastoolkit-rs"); + assert_eq!(dep.version, Some("0.2.0".to_string())); + } + + #[test] + fn test_find_by_license() { + let mut deps = Dependencies::new(); + deps.extend(vec![ + Dependency::from(("pkg:cargo/ghastoolkit-rs@0.2.0", "MIT")), + Dependency::from(("pkg:pip/ghastoolkit@0.12.0", "Apache-2.0")), + ]); + assert_eq!(deps.len(), 2); + + let deps_license = deps.find_by_license(&License::MIT); + assert_eq!(deps_license.len(), 1); + let dep = deps_license + .first() + .expect("Failed to find dependency by license"); + assert_eq!(dep.name, "ghastoolkit-rs"); + assert_eq!(dep.manager, "cargo"); + } +} diff --git a/vendor/ghastoolkit/src/supplychain/dependency.rs b/vendor/ghastoolkit/src/supplychain/dependency.rs new file mode 100644 index 0000000..37bce4c --- /dev/null +++ b/vendor/ghastoolkit/src/supplychain/dependency.rs @@ -0,0 +1,113 @@ +use std::{collections::HashMap, fmt::Display, str::FromStr}; + +use purl::GenericPurl; + +use crate::{Repository, supplychain::licenses::Licenses}; + +/// Supply Chain Dependency struct used to represent a dependency in a supply chain. +/// +/// # Example +/// +/// ```rust +/// use ghastoolkit::Dependency; +/// // Create a new Dependency from a PURL +/// let dependency = Dependency::from("pkg:generic/namespace/name@version"); +/// +/// println!("{}", dependency); +/// +/// ``` +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Dependency { + /// Manager / Type of the dependency + pub manager: String, + /// Name of the dependency + pub name: String, + /// Namespace of the dependency + pub namespace: Option, + /// Version of the dependency + pub version: Option, + /// Path to the dependency + path: Option, + /// Qualifiers for the dependency + qualifiers: HashMap, + /// SPDX licenses for the dependency + pub licenses: Licenses, + + repository: Option, + /// PURL + purl: Option>, +} + +impl Dependency { + /// Create a new Dependency + pub fn new() -> Self { + Default::default() + } + + /// Get the PURL for the dependency + pub fn purl(&self) -> String { + if let Some(purl) = &self.purl { + purl.to_string() + } else { + format!("pkg:{}@{}", self.manager, self.name) + } + } +} + +impl Display for Dependency { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.purl()) + } +} + +impl From<&str> for Dependency { + fn from(value: &str) -> Self { + Dependency::from(GenericPurl::::from_str(value).expect("Failed to parse PURL")) + } +} + +impl From for Dependency { + fn from(value: String) -> Self { + Dependency::from(value.as_str()) + } +} + +impl From> for Dependency { + fn from(value: GenericPurl) -> Self { + Dependency { + name: value.name().to_string(), + namespace: value.namespace().map(|s| s.to_string()), + version: value.version().map(|s| s.to_string()), + manager: value.package_type().clone(), + purl: Some(value), + ..Default::default() + } + } +} + +impl From<(&str, &str)> for Dependency { + fn from(value: (&str, &str)) -> Self { + let mut dependency = Dependency::from(value.0); + dependency.licenses = Licenses::from(value.1); + dependency + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dependency_from_str() { + let dependency = Dependency::from("pkg:generic/namespace/name@version"); + assert_eq!(dependency.name, "name"); + assert_eq!(dependency.namespace, Some("namespace".to_string())); + assert_eq!(dependency.version, Some("version".to_string())); + assert_eq!(dependency.manager, "generic".to_string()); + + assert_eq!( + dependency.purl(), + "pkg:generic/namespace/name@version".to_string() + ); + } +} diff --git a/vendor/ghastoolkit/src/supplychain/license.rs b/vendor/ghastoolkit/src/supplychain/license.rs new file mode 100644 index 0000000..90f640b --- /dev/null +++ b/vendor/ghastoolkit/src/supplychain/license.rs @@ -0,0 +1,107 @@ +use serde::{Deserialize, Serialize}; + +/// A Dependency License enum with SPDX and custom licenses. We only support a few licenses +/// but you can use the `Custom` variant to add your own license. +/// SPDX License List: https://spdx.org/licenses/ +/// +/// # Example +/// +/// ```rust +/// use ghastoolkit::supplychain::License; +/// +/// let license = License::from("MIT"); +/// assert_eq!(license, License::MIT); +/// +/// ``` +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub enum License { + /// Apache (1.0, 1.1, 2.0) + Apache(String), + /// MIT + MIT, + /// GPL (1.0, 2.0, 3.0) + GPL(String), + /// LGPL (2.0, 2.1, 3.0) + LGPL(String), + /// AGPL (1.0, 3.0) + AGPL(String), + /// Mozilla Public License (1.0, 1.1, 2.0) + MPL(String), + /// BSD (2-clause, 3-clause, 4-clause) + BSD(String), + /// CC0 + CC0, + /// ISC + ISC, + /// Custom license + Custom(String), + /// Unknown license + #[default] + Unknown, +} + +impl From<&str> for License { + fn from(value: &str) -> Self { + match value.to_lowercase().as_str() { + // apache-1.0 or apache-2.0 + value if value.contains("apache") => License::Apache(split_or_default(value, "-")), + value if value.contains("mit") => License::MIT, + value if value.starts_with("gpl") => License::GPL(split_or_default(value, "-")), + value if value.starts_with("lgpl") => License::LGPL(split_or_default(value, "-")), + value if value.starts_with("agpl") => License::AGPL(split_or_default(value, "-")), + value if value.starts_with("mpl") => License::MPL(split_or_default(value, "-")), + value if value.starts_with("bsd") => License::BSD(split_or_default(value, "-")), + "cc0" => License::CC0, + "isc" => License::ISC, + _ => License::Custom(String::from(value)), + } + } +} + +/// This helper function will split a string by a separator and return +/// the second part or the default value (the same string). +fn split_or_default(value: &str, sep: &str) -> String { + if let Some((_, version)) = value.split_once(sep) { + String::from(version.trim()) + } else { + String::from(value) + } +} + +impl From for License { + fn from(value: String) -> Self { + License::from(value.as_str()) + } +} + +#[cfg(test)] +mod tests { + use super::License; + + #[test] + fn test_license_from_str() { + let license = License::from("Apache-2.0"); + assert_eq!(license, License::Apache(String::from("2.0"))); + } + + #[test] + fn test_license_versions() { + let license = License::from("GPL-3.0"); + assert_eq!(license, License::GPL(String::from("3.0"))); + + let license = License::from("AGPL-3.0"); + assert_eq!(license, License::AGPL(String::from("3.0"))); + + let license = License::from("MPL-3.0"); + assert_eq!(license, License::MPL(String::from("3.0"))); + } + + #[test] + fn test_split_or_default() { + let license = super::split_or_default("Apache-2.0", "-"); + assert_eq!(license, "2.0"); + + let license = super::split_or_default("MIT", "-"); + assert_eq!(license, "MIT"); + } +} diff --git a/vendor/ghastoolkit/src/supplychain/licenses.rs b/vendor/ghastoolkit/src/supplychain/licenses.rs new file mode 100644 index 0000000..991fbed --- /dev/null +++ b/vendor/ghastoolkit/src/supplychain/licenses.rs @@ -0,0 +1,155 @@ +use serde::{Deserialize, Serialize}; + +use crate::supplychain::License; + +/// List of Licenses for a dependency +/// +/// # Example +/// +/// ```rust +/// use ghastoolkit::supplychain::Licenses; +/// +/// // Parse a string into a list of licenses +/// let licenses = Licenses::from("MIT, Apache-2.0"); +/// # assert_eq!(licenses.len(), 2); +/// // Do some with the licenses +/// +/// ``` +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct Licenses { + licenses: Vec, +} + +impl Licenses { + /// Create a new list of licenses + pub fn new() -> Self { + Self { + licenses: Vec::new(), + } + } + + /// Push a new license to the list + pub fn push(&mut self, license: License) { + self.licenses.push(license); + } + + /// Check if the list of licenses is empty + pub fn is_empty(&self) -> bool { + self.licenses.is_empty() + } + + /// Get the length of the list of licenses + pub fn len(&self) -> usize { + self.licenses.len() + } + + /// Check if the list contains a particular license + pub fn contains(&self, license: &License) -> bool { + self.licenses.contains(license) + } + + /// Parse a string into a list of licenses. + /// It will split the string by "and" or "," + pub fn parse(value: &str) -> Licenses { + match value.to_lowercase().as_str() { + value if value.contains("and") => Licenses::parse_sep(value, "and"), + value if value.contains(',') => Licenses::parse_sep(value, ","), + _ => { + let mut licenses = Licenses::new(); + licenses.push(License::from(value)); + licenses + } + } + } + + fn parse_sep(value: &str, sep: &str) -> Licenses { + let mut licenses = Licenses::new(); + for license in value.split(sep) { + licenses.push(License::from(license.trim())); + } + licenses + } +} + +impl IntoIterator for Licenses { + type Item = License; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.licenses.into_iter() + } +} + +impl From<&str> for Licenses { + fn from(value: &str) -> Self { + Licenses::parse(value) + } +} + +impl From for Licenses { + fn from(value: String) -> Self { + Licenses::parse(value.as_str()) + } +} + +impl From> for Licenses { + fn from(value: Vec<&str>) -> Self { + let mut licenses = Licenses::new(); + for license in value { + licenses.push(License::from(license)); + } + licenses + } +} + +#[cfg(test)] +mod tests { + use crate::supplychain::{License, Licenses}; + + #[test] + fn test_single_license() { + let licenses = Licenses::from("Apache-2.0"); + + let correct = Licenses { + licenses: vec![License::Apache(String::from("2.0"))], + }; + + assert_eq!(licenses, correct); + assert_eq!(licenses.len(), 1); + } + + #[test] + fn test_licenses_from_str() { + let licenses = Licenses::from("Apache-2.0 AND MIT"); + + let correct = Licenses { + licenses: vec![License::Apache(String::from("2.0")), License::MIT], + }; + + assert_eq!(licenses, correct); + assert_eq!(licenses.len(), 2); + } + + #[test] + fn test_licenses_from_vec() { + let licenses = Licenses::from(vec!["Apache-2.0", "MIT"]); + + let correct = Licenses { + licenses: vec![License::Apache(String::from("2.0")), License::MIT], + }; + + assert_eq!(licenses, correct); + assert_eq!(licenses.len(), 2); + } + + #[test] + fn test_licenses_commasep() { + let licenses = Licenses::from("Apache-2.0, MIT"); + + let correct = Licenses { + licenses: vec![License::Apache(String::from("2.0")), License::MIT], + }; + + assert_eq!(licenses, correct); + } +} diff --git a/vendor/ghastoolkit/src/supplychain/mod.rs b/vendor/ghastoolkit/src/supplychain/mod.rs new file mode 100644 index 0000000..ae665e0 --- /dev/null +++ b/vendor/ghastoolkit/src/supplychain/mod.rs @@ -0,0 +1,17 @@ +//! # GHASToolkit supplychain module +//! +//! This contains all the supplychain related functions and helpers + +/// This module contains the dependencies +pub mod dependencies; +/// This module contains the dependency +pub mod dependency; +/// This module contains the license +pub mod license; +/// This module contains the licenses +pub mod licenses; + +pub use dependencies::Dependencies; +pub use dependency::Dependency; +pub use license::License; +pub use licenses::Licenses; diff --git a/vendor/ghastoolkit/src/utils/mod.rs b/vendor/ghastoolkit/src/utils/mod.rs new file mode 100644 index 0000000..792fe90 --- /dev/null +++ b/vendor/ghastoolkit/src/utils/mod.rs @@ -0,0 +1,6 @@ +//! # GHASToolkit utils module +//! +//! This contains all the utility functions and helpers + +/// Module for SARIF related utilities +pub mod sarif; diff --git a/vendor/ghastoolkit/src/utils/sarif.rs b/vendor/ghastoolkit/src/utils/sarif.rs new file mode 100644 index 0000000..05f0308 --- /dev/null +++ b/vendor/ghastoolkit/src/utils/sarif.rs @@ -0,0 +1,214 @@ +use std::{ + fmt::{Display, Formatter}, + path::PathBuf, +}; + +use serde::{Deserialize, Serialize}; + +use crate::GHASError; + +/// Sarif Structure +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Sarif { + /// Schema URL + #[serde(rename = "$schema")] + pub schema: String, + /// Schema Version + pub version: String, + /// Runs + pub runs: Vec, +} + +impl TryFrom for Sarif { + type Error = GHASError; + + fn try_from(value: PathBuf) -> Result { + // Read and load SARIF file + let file = std::fs::File::open(value)?; + let reader = std::io::BufReader::new(file); + let sarif: Sarif = serde_json::from_reader(reader)?; + Ok(sarif) + } +} + +impl TryFrom for Sarif { + type Error = GHASError; + fn try_from(value: String) -> Result { + Sarif::try_from(PathBuf::from(value)) + } +} + +impl Sarif { + /// Create a new SARIF object + pub fn new() -> Self { + Sarif { + schema: String::from( + "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + ), + version: String::from("2.1.0"), + runs: vec![], + } + } + + /// Get Results from all runs + pub fn get_results(&self) -> Vec { + let mut results = vec![]; + for run in &self.runs { + results.extend(run.results.clone()); + } + results + } + + /// Write SARIF to file + pub fn write(&self, path: PathBuf) -> Result<(), GHASError> { + let file = std::fs::File::create(path)?; + let writer = std::io::BufWriter::new(file); + serde_json::to_writer_pretty(writer, self)?; + Ok(()) + } +} + +/// Sarif Run +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SarifRun { + /// Tool + pub tool: SarifTool, + /// Results + pub results: Vec, +} + +/// Sarif Result +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SarifResult { + /// Rule ID + #[serde(rename = "ruleId")] + pub rule_id: String, + /// Rule Index + #[serde(rename = "ruleIndex")] + pub rule_index: i32, + /// Rule + pub rule: SarifRule, + /// Level + #[serde(default)] + pub level: String, + /// Message + pub message: SarifMessage, + /// Locations + pub locations: Vec, +} + +impl Display for SarifResult { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.rule_id, self.message.text) + } +} + +/// SARIF Rule +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SarifRule { + /// ID + pub id: String, + /// Index + pub index: i32, +} + +/// SARIF Location +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SarifLocation { + /// Physical Location + #[serde(rename = "physicalLocation")] + pub physical_location: SarifPhysicalLocation, +} + +/// SARIF Physical Location +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SarifPhysicalLocation { + /// Artifact Location + #[serde(rename = "artifactLocation")] + pub artifact_location: SarifArtifactLocation, + /// Region + pub region: SarifRegion, +} + +/// SARIF Artifact Location +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SarifArtifactLocation { + /// URI + pub uri: String, + /// URI Base ID + #[serde(rename = "uriBaseId")] + pub uri_base_id: String, + /// ID + #[serde(default)] + pub id: i32, +} + +/// SARIF Region +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SarifRegion { + /// Start Line + #[serde(rename = "startLine")] + pub start_line: i32, + /// Start Column + #[serde(default, rename = "startColumn")] + pub start_column: i32, + /// End Line + #[serde(rename = "endLine")] + pub end_line: Option, + /// End Column + #[serde(rename = "endColumn")] + pub end_column: Option, +} + +/// SARIF Tool +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SarifTool { + /// Driver + pub driver: SarifToolDriver, +} + +impl Display for SarifTool { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if let Some(version) = &self.driver.version { + write!(f, "{} v{}", self.driver.name, version) + } else { + write!(f, "{}", self.driver.name) + } + } +} + +/// SARIF Tool Driver +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SarifToolDriver { + /// Name + pub name: String, + /// Organization + pub organization: Option, + /// Version + #[serde(rename = "semanticVersion")] + pub version: Option, + /// Notifications + pub notifications: Option>, +} + +/// SARIF Tool Driver Notification +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SarifToolDriverNotification { + /// Identifier + pub id: String, + /// Name + pub name: String, + /// Short Description + #[serde(rename = "shortDescription")] + pub short_description: SarifMessage, + /// Full Description + #[serde(rename = "fullDescription")] + pub full_description: SarifMessage, +} + +/// SARIF Message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SarifMessage { + /// Text + pub text: String, +} From d869ab70ef63e5f64a7a72968d38c640a206c23b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 22:13:49 +0000 Subject: [PATCH 3/3] fix: patch ghastoolkit to fix compilation errors with ghactions-toolcache 0.18.4 Co-authored-by: felickz <1760475+felickz@users.noreply.github.com> --- Cargo.lock | 67 ++--------------------- Cargo.toml | 6 ++ vendor/ghastoolkit/Cargo.toml | 2 +- vendor/ghastoolkit/src/codeql/download.rs | 6 +- 4 files changed, 16 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1f08f32..3d89c1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -272,7 +272,7 @@ dependencies = [ "ghastoolkit", "glob", "log", - "octocrab 0.48.1", + "octocrab", "openssl", "serde_json", "thiserror", @@ -812,7 +812,7 @@ dependencies = [ "http", "indexmap", "log", - "octocrab 0.48.1", + "octocrab", "serde", "serde_yaml", "thiserror", @@ -844,7 +844,7 @@ dependencies = [ "glob", "http", "log", - "octocrab 0.48.1", + "octocrab", "reqwest", "tar", "thiserror", @@ -856,8 +856,6 @@ dependencies = [ [[package]] name = "ghastoolkit" version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4430b3fa62adef1df9463e8d51f3fc8de6db14adb138c781c2c11627932f881b" dependencies = [ "anyhow", "async-trait", @@ -867,7 +865,7 @@ dependencies = [ "glob", "http", "log", - "octocrab 0.47.1", + "octocrab", "purl", "regex", "reqwest", @@ -1327,21 +1325,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonwebtoken" -version = "9.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" -dependencies = [ - "base64", - "js-sys", - "pem", - "ring", - "serde", - "serde_json", - "simple_asn1", -] - [[package]] name = "jsonwebtoken" version = "10.3.0" @@ -1601,46 +1584,6 @@ dependencies = [ "libm", ] -[[package]] -name = "octocrab" -version = "0.47.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f50b2657b7e31c849c612c4ca71527861631fe3c392f931fb28990b045f972" -dependencies = [ - "arc-swap", - "async-trait", - "base64", - "bytes", - "cfg-if", - "chrono", - "either", - "futures", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-timeout", - "hyper-util", - "jsonwebtoken 9.3.1", - "once_cell", - "percent-encoding", - "pin-project", - "secrecy", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "snafu", - "tokio", - "tower", - "tower-http", - "tracing", - "url", - "web-time", -] - [[package]] name = "octocrab" version = "0.48.1" @@ -1665,7 +1608,7 @@ dependencies = [ "hyper-rustls", "hyper-timeout", "hyper-util", - "jsonwebtoken 10.3.0", + "jsonwebtoken", "once_cell", "percent-encoding", "pin-project", diff --git a/Cargo.toml b/Cargo.toml index 7a598bb..cae0afa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,3 +31,9 @@ ghastoolkit = { version = "^0.12", features = ["toolcache"] } octocrab = "^0.48" openssl = { version = "0.10", features = ["vendored"] } serde_json = "1.0" + +# Patch ghastoolkit to fix compilation errors with ghactions-toolcache 0.18.4: +# 1. Double reference bug: `&asset` -> `asset` in download_asset call +# 2. Missing error conversion: ToolCacheError -> GHASError via map_err +[patch.crates-io] +ghastoolkit = { path = "vendor/ghastoolkit" } diff --git a/vendor/ghastoolkit/Cargo.toml b/vendor/ghastoolkit/Cargo.toml index 29b206e..80cc03b 100644 --- a/vendor/ghastoolkit/Cargo.toml +++ b/vendor/ghastoolkit/Cargo.toml @@ -78,7 +78,7 @@ version = "1.3" version = "0.4" [dependencies.octocrab] -version = "^0.47" +version = "^0.48" [dependencies.purl] version = "0.1" diff --git a/vendor/ghastoolkit/src/codeql/download.rs b/vendor/ghastoolkit/src/codeql/download.rs index 0d6d5da..3f31aae 100644 --- a/vendor/ghastoolkit/src/codeql/download.rs +++ b/vendor/ghastoolkit/src/codeql/download.rs @@ -78,11 +78,13 @@ impl CodeQL { "Downloading CodeQL CLI from GitHub: {}", asset.browser_download_url ); - toolcache.download_asset(&asset, &codeql_archive).await?; + toolcache.download_asset(asset, &codeql_archive).await + .map_err(|e| GHASError::CodeQLError(e.to_string()))?; } log::info!("Extracting asset to {:?}", path); - toolcache.extract_archive(&codeql_archive, &path).await?; + toolcache.extract_archive(&codeql_archive, &path).await + .map_err(|e| GHASError::CodeQLError(e.to_string()))?; let codeql_dir = path.join("codeql"); if !codeql_dir.exists() {