Skip to content

Commit 6705ca6

Browse files
committed
feat: Add support for multiple extractors
1 parent 0fd4378 commit 6705ca6

File tree

4 files changed

+167
-83
lines changed

4 files changed

+167
-83
lines changed

action.yml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ branding:
99
inputs:
1010
token:
1111
description: GitHub Token
12-
extractor:
13-
description: GitHub Repository where the extractor is located
12+
extractors:
13+
description: GitHub Repository where the extractor(s) is located
1414
required: true
15-
language:
15+
languages:
1616
description: Language(s) to use
1717
required: true
1818
packs:
19-
description: Query Packs to use
19+
description: Query Pack(s) to use
20+
allow-empty-database:
21+
description: Allow empty database
2022
codeql-version:
2123
description: CodeQL Version
2224
default: latest
@@ -26,6 +28,8 @@ inputs:
2628
description: Attestation
2729
default: 'false'
2830
outputs:
31+
sarif_results:
32+
description: SARIF Results Directory
2933
version:
3034
description: Version of the extractor to use
3135
extractor_path:

src/action.rs

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ use ghactions::prelude::*;
66
use ghactions_core::repository::reference::RepositoryReference as Repository;
77
use ghastoolkit::{CodeQL, codeql::CodeQLLanguage};
88

9+
pub const BANNER: &str = r#" ___ _ ____ __ __ _ _ _
10+
/ __\___ __| | ___ /___ \/ / /__\_ _| |_ /_\ ___| |_
11+
/ / / _ \ / _` |/ _ \// / / / /_\ \ \/ / __|//_\\ / __| __|
12+
/ /__| (_) | (_| | __/ \_/ / /___//__ > <| |_/ _ \ (__| |_
13+
\____/\___/ \__,_|\___\___,_\____/\__/ /_/\_\\__\_/ \_/\___|\__|"#;
14+
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
15+
pub const AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
16+
917
/// This action is for 3rd party CodeQL extractors to be used in GitHub Actions
1018
#[derive(Actions, Debug, Clone, Default)]
1119
#[action(
@@ -29,19 +37,29 @@ pub struct Action {
2937

3038
/// GitHub Repository where the extractor is located
3139
#[input(
32-
description = "GitHub Repository where the extractor is located",
40+
description = "GitHub Repository where the extractor(s) is located",
41+
split = ",",
3342
required = true
3443
)]
35-
extractor: String,
44+
extractors: Vec<String>,
3645

3746
/// Language(d) to use
3847
#[input(description = "Language(s) to use", split = ",", required = true)]
39-
language: Vec<String>,
48+
languages: Vec<String>,
4049

4150
/// Queries packs to use
42-
#[input(description = "Query Packs to use", split = ",")]
51+
#[input(description = "Query Pack(s) to use", split = ",")]
4352
packs: Vec<String>,
4453

54+
/// Allow empty database. This allows for an extractor to error out if no database was
55+
/// created dur to no source code being found for that language.
56+
#[input(
57+
description = "Allow empty database",
58+
default = false,
59+
rename = "allow-empty-database"
60+
)]
61+
allow_empty_database: bool,
62+
4563
/// CodeQL Version
4664
#[input(
4765
description = "CodeQL Version",
@@ -62,6 +80,10 @@ pub struct Action {
6280
#[input(description = "Attestation", default = "false")]
6381
attestation: bool,
6482

83+
/// SARIF Results Directory
84+
#[output(description = "SARIF Results Directory", rename = "sarif-results")]
85+
sarif_results: String,
86+
6587
/// Version of the extractor to use
6688
#[output(description = "Version of the extractor to use")]
6789
version: String,
@@ -78,31 +100,37 @@ impl Action {
78100
return std::env::current_dir().context("Failed to get current directory");
79101
}
80102
log::debug!("Using the provided working directory");
81-
Ok(std::path::PathBuf::from(&self.working_directory)
103+
std::path::PathBuf::from(&self.working_directory)
82104
.canonicalize()
83105
.context(format!(
84106
"Failed to get working directory `{}`",
85107
self.working_directory
86-
))?)
108+
))
87109
}
88110

89111
/// Gets the repository to use for the extractor. If the repository is not provided,
90112
/// it will use the repository that the action is running in.
91-
pub fn extractor_repository(&self) -> Result<Repository> {
92-
let repo = if self.extractor.is_empty() {
113+
pub fn extractor_repository(&self) -> Result<Vec<Repository>> {
114+
if self.extractors.is_empty() {
93115
log::debug!("No extractor repository provided, using the current repository");
94-
self.get_repository()?
95-
} else {
96-
log::debug!("Using the provided extractor repository");
97-
self.extractor.clone()
98-
};
99-
log::info!("Extractor Repository :: {}", repo);
100-
101-
Ok(Repository::parse(&repo)?)
116+
return Ok(vec![Repository::parse(&self.get_repository()?)?]);
117+
}
118+
119+
log::debug!("Using the provided extractor repository");
120+
121+
Ok(self
122+
.extractors
123+
.iter()
124+
.filter_map(|ext| {
125+
Repository::parse(ext)
126+
.context(format!("Failed to parse extractor repository `{ext}`"))
127+
.ok()
128+
})
129+
.collect::<Vec<Repository>>())
102130
}
103131

104132
pub fn languages(&self) -> Vec<CodeQLLanguage> {
105-
self.language
133+
self.languages
106134
.iter()
107135
.map(|lang| CodeQLLanguage::from(lang.as_str()))
108136
.collect()
@@ -111,10 +139,10 @@ impl Action {
111139
pub fn validate_languages(&self, codeql_languages: &Vec<CodeQLLanguage>) -> Result<()> {
112140
for lang in self.languages() {
113141
let mut supported = false;
114-
log::debug!("Validating language `{}`", lang);
142+
log::debug!("Validating language `{lang}`");
115143
for codeql_lang in codeql_languages {
116144
if lang.language().to_lowercase() == codeql_lang.language().to_lowercase() {
117-
log::debug!("Language `{}` is supported", lang);
145+
log::debug!("Language `{lang}` is supported");
118146
supported = true;
119147
break;
120148
}
@@ -144,19 +172,23 @@ impl Action {
144172
pub async fn install_packs(&self, codeql: &CodeQL) -> Result<()> {
145173
log::info!("Installing CodeQL Packs");
146174
for pack in &self.packs {
147-
log::info!("Installing pack `{}`", pack);
175+
log::info!("Installing pack `{pack}`");
148176

149177
codeql
150178
.run(vec!["pack", "download", pack])
151179
.await
152-
.context(format!("Failed to download pack `{}`", pack))?;
180+
.context(format!("Failed to download pack `{pack}`"))?;
153181
}
154182
Ok(())
155183
}
156184

157185
pub fn attestation(&self) -> bool {
158186
self.attestation
159187
}
188+
189+
pub fn allow_empty_database(&self) -> bool {
190+
self.allow_empty_database
191+
}
160192
}
161193

162194
#[cfg(test)]
@@ -165,8 +197,8 @@ mod tests {
165197

166198
fn action() -> Action {
167199
Action {
168-
extractor: "owner/repo".to_string(),
169-
language: vec!["iac".to_string()],
200+
extractors: vec!["owner/repo".to_string()],
201+
languages: vec!["iac".to_string()],
170202
..Default::default()
171203
}
172204
}

src/extractors.rs

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::path::PathBuf;
22

3-
use anyhow::Result;
3+
use anyhow::{Context, Result};
44
use ghactions_core::repository::reference::RepositoryReference as Repository;
55
use octocrab::models::repos::{Asset, Release};
66

@@ -34,14 +34,16 @@ pub async fn fetch_extractor(
3434
attest: bool,
3535
output: &PathBuf,
3636
) -> Result<PathBuf> {
37-
let extractor_tarball = output.join("extractor.tar.gz");
38-
let extractor_pack = output.join("extractor-pack");
39-
let extractor_path = extractor_pack.join("codeql-extractor.yml");
37+
let extractor_tarball = output.join(format!("{}.tar.gz", &repository.name));
38+
log::debug!("Extractor Tarball :: {extractor_tarball:?}");
39+
let extractor_pack = output.join(&repository.name);
40+
41+
log::info!("Extractor Path :: {extractor_pack:?}");
4042

4143
let toolcache = ghactions::ToolCache::new();
4244

4345
if !extractor_tarball.exists() {
44-
log::info!("Downloading asset to {:?}", extractor_tarball);
46+
log::info!("Downloading asset to {extractor_tarball:?}");
4547

4648
let release = fetch_releases(client, repository).await?;
4749

@@ -53,11 +55,15 @@ pub async fn fetch_extractor(
5355

5456
let asset: Asset = client.get(release_asset.url.clone(), None::<&()>).await?;
5557

56-
toolcache.download_asset(&asset, &extractor_tarball).await?;
58+
toolcache
59+
.download_asset(&asset, &extractor_tarball)
60+
.await
61+
.context(format!("Extractor Archive: {extractor_tarball:?}"))
62+
.context("Failed to download extractor")?;
5763
}
5864

5965
if attest {
60-
log::info!("Attesting asset {:?}", extractor_tarball);
66+
log::info!("Attesting asset {extractor_tarball:?}");
6167

6268
let output = tokio::process::Command::new("gh")
6369
.arg("attestation")
@@ -80,15 +86,31 @@ pub async fn fetch_extractor(
8086
}
8187

8288
if !extractor_pack.exists() {
83-
log::info!("Extracting asset to {:?}", extractor_path);
89+
log::info!("Extracting asset to {extractor_pack:?}");
90+
8491
toolcache
85-
.extract_archive(&extractor_tarball, &output)
86-
.await?;
92+
.extract_archive(&extractor_tarball, &extractor_pack)
93+
.await
94+
.context(format!("Extractor Archive: {extractor_tarball:?}"))
95+
.context("Failed to extract extractor")?;
96+
}
8797

88-
if !extractor_path.exists() {
89-
return Err(anyhow::anyhow!("Extractor not found"));
98+
// Find `codeql-extractor.yml` in the extracted directory using glob
99+
for glob in glob::glob(
100+
&extractor_pack
101+
.join("**/codeql-extractor.yml")
102+
.to_string_lossy(),
103+
)? {
104+
match glob {
105+
Ok(path) => {
106+
log::debug!("Extractor Path :: {path:?}");
107+
return Ok(path.parent().unwrap().to_path_buf().canonicalize()?);
108+
}
109+
Err(e) => {
110+
log::error!("Failed to find extractor: {e}");
111+
return Err(anyhow::anyhow!("Failed to find extractor: {e}"));
112+
}
90113
}
91114
}
92-
93-
Ok(extractor_pack.canonicalize()?)
115+
Ok(extractor_pack)
94116
}

0 commit comments

Comments
 (0)