Skip to content

Commit fc6a3f6

Browse files
committed
feat: Add multiple extractor support
1 parent 62f9dcc commit fc6a3f6

File tree

3 files changed

+117
-42
lines changed

3 files changed

+117
-42
lines changed

action.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@ inputs:
1010
token:
1111
description: GitHub Token
1212
extractor:
13-
description: GitHub Repository where the extractor is located
13+
description: GitHub Repositories where the extractors are located
1414
required: true
1515
language:
1616
description: Language(s) to use
17-
required: true
1817
attestation:
1918
description: Attestation
2019
default: 'false'

src/action.rs

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,16 @@ pub struct Action {
2525
#[input(description = "GitHub Token")]
2626
token: String,
2727

28-
/// GitHub Repository where the extractor is located
28+
/// GitHub Repositories where the extractors are located
2929
#[input(
30-
description = "GitHub Repository where the extractor is located",
31-
required = true
30+
description = "GitHub Repositories where the extractors are located",
31+
required = true,
32+
split = ","
3233
)]
33-
extractor: String,
34+
extractor: Vec<String>,
3435

35-
/// Language(d) to use
36-
#[input(description = "Language(s) to use", split = ",", required = true)]
36+
/// Language(d) to use, e.g. `iac`, `javascript`, `python`, etc.
37+
#[input(description = "Language(s) to use", split = ",")]
3738
language: Vec<String>,
3839

3940
/// Attestation
@@ -49,19 +50,22 @@ pub struct Action {
4950
}
5051

5152
impl Action {
52-
/// Gets the repository to use for the extractor. If the repository is not provided,
53+
/// Gets the repositories to use for the extractors. If no repositories are provided,
5354
/// it will use the repository that the action is running in.
54-
pub fn extractor_repository(&self) -> Result<Repository> {
55-
let repo = if self.extractor.is_empty() {
56-
log::debug!("No extractor repository provided, using the current repository");
57-
self.get_repository()?
58-
} else {
59-
log::debug!("Using the provided extractor repository");
60-
self.extractor.clone()
61-
};
62-
log::info!("Extractor Repository :: {}", repo);
63-
64-
Ok(Repository::parse(&repo)?)
55+
pub fn extractor_repositories(&self) -> Result<Vec<Repository>> {
56+
let mut repositories = Vec::new();
57+
for extractor in &self.extractor {
58+
let repo = if extractor.is_empty() {
59+
log::debug!("No extractor repository provided, using the current repository");
60+
self.get_repository()?
61+
} else {
62+
log::debug!("Using the provided extractor repository");
63+
extractor.clone()
64+
};
65+
log::info!("Extractor Repository :: {}", repo);
66+
repositories.push(Repository::parse(&repo)?);
67+
}
68+
Ok(repositories)
6569
}
6670

6771
pub fn languages(&self) -> Vec<CodeQLLanguage> {
@@ -107,12 +111,63 @@ mod tests {
107111

108112
fn action() -> Action {
109113
Action {
110-
extractor: "owner/repo".to_string(),
114+
extractor: vec!["owner/repo1".to_string(), "owner/repo2".to_string()],
115+
language: vec!["iac".to_string()],
116+
..Default::default()
117+
}
118+
}
119+
120+
fn action_with_extractors(extractors: Vec<String>) -> Action {
121+
Action {
122+
extractor: extractors,
111123
language: vec!["iac".to_string()],
112124
..Default::default()
113125
}
114126
}
115127

128+
#[test]
129+
fn test_extractor_repositories() {
130+
let action = action();
131+
let repositories = action.extractor_repositories().unwrap();
132+
assert_eq!(repositories.len(), 2);
133+
assert_eq!(repositories[0].to_string(), "owner/repo1");
134+
assert_eq!(repositories[1].to_string(), "owner/repo2");
135+
}
136+
137+
#[test]
138+
fn test_extractor_repositories_multiple() {
139+
let action = action_with_extractors(vec![
140+
"owner/repo1".to_string(),
141+
"owner/repo2".to_string(),
142+
]);
143+
let repositories = action.extractor_repositories().unwrap();
144+
assert_eq!(repositories.len(), 2);
145+
assert_eq!(repositories[0].to_string(), "owner/repo1");
146+
assert_eq!(repositories[1].to_string(), "owner/repo2");
147+
}
148+
149+
#[test]
150+
fn test_extractor_repositories_single() {
151+
let action = action_with_extractors(vec!["owner/repo1".to_string()]);
152+
let repositories = action.extractor_repositories().unwrap();
153+
assert_eq!(repositories.len(), 1);
154+
assert_eq!(repositories[0].to_string(), "owner/repo1");
155+
}
156+
157+
#[test]
158+
fn test_extractor_repositories_empty() {
159+
let action = action_with_extractors(vec![]);
160+
let result = action.extractor_repositories();
161+
assert!(result.is_err(), "Expected error for empty extractor list");
162+
}
163+
164+
#[test]
165+
fn test_extractor_repositories_invalid_format() {
166+
let action = action_with_extractors(vec!["invalid_repo_format".to_string()]);
167+
let result = action.extractor_repositories();
168+
assert!(result.is_err(), "Expected error for invalid repository format");
169+
}
170+
116171
#[test]
117172
fn test_validate_languages() {
118173
let action = action();

src/main.rs

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ use std::path::PathBuf;
33
use anyhow::{Context, Result};
44
use ghactions::{ActionTrait, ToolCache, group, groupend};
55
use ghastoolkit::codeql::database::queries::CodeQLQueries;
6-
use ghastoolkit::{CodeQL, CodeQLDatabase};
6+
use ghastoolkit::codeql::CodeQLLanguage;
7+
use ghastoolkit::{CodeQL, CodeQLDatabase, CodeQLExtractor};
78
use log::{debug, info};
89

910
mod action;
@@ -24,8 +25,8 @@ async fn main() -> Result<()> {
2425
debug!("ToolCache :: {:?}", toolcache);
2526

2627
// Extractor
27-
let extractor_repo = action.extractor_repository()?;
28-
info!("Extractor Repository :: {}", extractor_repo);
28+
let extractor_repos = action.extractor_repositories()?;
29+
info!("Extractor Repositories :: {:?}", extractor_repos);
2930

3031
let extractor_path = PathBuf::from("./extractors");
3132
if !extractor_path.exists() {
@@ -34,21 +35,31 @@ async fn main() -> Result<()> {
3435
info!("Created Extractor Directory :: {:?}", extractor_path);
3536
}
3637

37-
let extractor = extractors::fetch_extractor(
38-
&client,
39-
&extractor_repo,
40-
action.attestation(),
41-
&extractor_path,
42-
)
43-
.await
44-
.context("Failed to fetch extractor")?;
45-
log::info!("Extractor :: {:?}", extractor);
46-
47-
let codeql = CodeQL::init()
48-
.search_path(extractor)
49-
.build()
38+
let mut codeql_builder = CodeQL::init();
39+
let mut extractors = Vec::new();
40+
41+
// Download and extract the extractor repositories
42+
for extractor_repo in extractor_repos {
43+
let extractor = extractors::fetch_extractor(
44+
&client,
45+
&extractor_repo,
46+
action.attestation(),
47+
&extractor_path,
48+
)
5049
.await
51-
.context("Failed to create CodeQL instance")?;
50+
.context("Failed to fetch extractor")?;
51+
log::info!("Extractor :: {:?}", extractor);
52+
53+
codeql_builder = codeql_builder.search_path(extractor_path.clone());
54+
55+
extractors.push((extractor_repo.clone(), CodeQLExtractor::load_path(extractor.clone())?));
56+
}
57+
58+
let codeql = codeql_builder
59+
.build()
60+
.await
61+
.context("Failed to create CodeQL instance")?;
62+
5263
log::info!("CodeQL :: {:?}", codeql);
5364

5465
let languages = codeql.get_languages().await?;
@@ -72,13 +83,22 @@ async fn main() -> Result<()> {
7283

7384
std::fs::create_dir_all(&sarif_output)?;
7485

75-
for language in action.languages() {
76-
let group = format!("Running {} extractor", language.language());
86+
for (extractor_repo, extractor) in extractors.iter() {
87+
let language = CodeQLLanguage::from(extractor.name.clone());
88+
89+
if !action.languages().is_empty() {
90+
if !action.languages().contains(&language) {
91+
log::info!("Skipping language :: {}", language);
92+
continue;
93+
}
94+
}
95+
96+
let group = format!("Running `{}` extractor", language.language());
7797
group!(group);
7898

7999
log::info!("Running extractor for language :: {}", language);
80100

81-
let database_path = databases.join(format!("db-{}", language));
101+
let database_path = databases.join(format!("db-{}", language.language()));
82102
let sarif_path = sarif_output.join(format!("{}-results.sarif", language.language()));
83103

84104
let database = CodeQLDatabase::init()
@@ -92,9 +112,10 @@ async fn main() -> Result<()> {
92112
codeql.database(&database).overwrite().create().await?;
93113
log::info!("Created database :: {:?}", database);
94114

115+
// TODO: Assumes the queries are in the same org
95116
let queries = CodeQLQueries::from(format!(
96117
"{}/{}-queries",
97-
extractor_repo.owner.clone(),
118+
extractor_repo.owner,
98119
language.language()
99120
));
100121
log::debug!("Queries :: {:?}", queries);

0 commit comments

Comments
 (0)