1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
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<String>,
/// Enterprise account name (if applicable)
enterprise: Option<String>,
/// GitHub token (personal access token or GitHub App token)
token: Option<String>,
/// 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<String, GHASError> {
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<GitHubLanguages> {
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<GitRepository, GHASError> {
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<String>,
enterprise: Option<String>,
token: Option<String>,
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<GitHub, GHASError> {
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");
}
}