Skip to content

Commit 007b856

Browse files
authored
Add phylum exception subcommand (#1557)
This patch adds a new `phylum exception` subcommand, with the three `add`, `remove`, and `list` commands under it. These new subcommands allow managing package exceptions to remove friction with Phylum's firewall. The add and remove subcommands are targeted at interactive use, pulling logs from the package firewall and existing exceptions to suggest what is most likely to be used for adding/removing an exception. While currently firewall logs are pulled for adding exceptions, the individual analysis results for each version are not listed in the suggestions. The `list` and `remove` subcommand allow managing both package and issue suppressions, however `add` currently only supports adding package exceptions. Closes #1552.
1 parent a0db974 commit 007b856

18 files changed

+1012
-66
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
88

99
## Unreleased
1010

11+
### Added
12+
13+
- `phylum exception` subcommand for managing suppressions
14+
1115
## 7.2.0 - 2024-12-10
1216

1317
### Added

Cargo.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "7.2.0"
44
authors = ["Phylum, Inc. <[email protected]>"]
55
license = "GPL-3.0-or-later"
66
edition = "2021"
7-
rust-version = "1.80.0"
7+
rust-version = "1.82.0"
88
autotests = false
99

1010
[[test]]
@@ -38,6 +38,7 @@ git2 = { version = "0.19.0", default-features = false }
3838
git-version = "0.3.5"
3939
home = "0.5.3"
4040
ignore = { version = "0.4.20", optional = true }
41+
indexmap = "2.7.0"
4142
lazy_static = "1.4.0"
4243
libc = "0.2.135"
4344
log = "0.4.6"
@@ -48,7 +49,7 @@ phylum_lockfile = { path = "../lockfile", features = ["generator"] }
4849
phylum_project = { path = "../phylum_project" }
4950
phylum_types = { git = "https://github.com/phylum-dev/phylum-types", branch = "development" }
5051
prettytable-rs = "0.10.0"
51-
purl = "0.1.1"
52+
purl = { version = "0.1.5", features = ["serde"] }
5253
rand = "0.8.4"
5354
regex = "1.5.5"
5455
reqwest = { version = "0.12.7", features = ["blocking", "json", "rustls-tls", "rustls-tls-native-roots", "rustls-tls-webpki-roots"], default-features = false }

cli/src/api/endpoints.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,119 @@ pub fn firewall_log(api_uri: &str) -> Result<Url, BaseUriError> {
197197
Ok(get_firewall_path(api_uri)?.join("activity")?)
198198
}
199199

200+
/// GET /organizations/<orgName>/groups/<groupName>/preferences.
201+
pub fn org_group_preferences(
202+
api_uri: &str,
203+
org_name: &str,
204+
group_name: &str,
205+
) -> Result<Url, BaseUriError> {
206+
let mut url = get_api_path(api_uri)?;
207+
url.path_segments_mut().unwrap().pop_if_empty().extend([
208+
"organizations",
209+
org_name,
210+
"groups",
211+
group_name,
212+
"preferences",
213+
]);
214+
Ok(url)
215+
}
216+
217+
/// GET /preferences/group/<groupName>
218+
pub fn group_preferences(api_uri: &str, group_name: &str) -> Result<Url, BaseUriError> {
219+
let mut url = get_api_path(api_uri)?;
220+
url.path_segments_mut().unwrap().pop_if_empty().extend(["preferences", "group", group_name]);
221+
Ok(url)
222+
}
223+
224+
/// GET /preferences/project/<projectId>
225+
pub fn project_preferences(api_uri: &str, project_id: &str) -> Result<Url, BaseUriError> {
226+
let mut url = get_api_path(api_uri)?;
227+
url.path_segments_mut().unwrap().pop_if_empty().extend(["preferences", "project", project_id]);
228+
Ok(url)
229+
}
230+
231+
/// POST /organizations/<orgName>/groups/<groupName>/suppress.
232+
pub fn org_group_suppress(
233+
api_uri: &str,
234+
org_name: &str,
235+
group_name: &str,
236+
) -> Result<Url, BaseUriError> {
237+
let mut url = get_api_path(api_uri)?;
238+
url.path_segments_mut().unwrap().pop_if_empty().extend([
239+
"organizations",
240+
org_name,
241+
"groups",
242+
group_name,
243+
"suppress",
244+
]);
245+
Ok(url)
246+
}
247+
248+
/// POST /preferences/group/<groupName>/suppress.
249+
pub fn group_suppress(api_uri: &str, group_name: &str) -> Result<Url, BaseUriError> {
250+
let mut url = get_api_path(api_uri)?;
251+
url.path_segments_mut().unwrap().pop_if_empty().extend([
252+
"preferences",
253+
"group",
254+
group_name,
255+
"suppress",
256+
]);
257+
Ok(url)
258+
}
259+
260+
/// POST /preferences/project/<projectId>/suppress.
261+
pub fn project_suppress(api_uri: &str, project_id: &str) -> Result<Url, BaseUriError> {
262+
let mut url = get_api_path(api_uri)?;
263+
url.path_segments_mut().unwrap().pop_if_empty().extend([
264+
"preferences",
265+
"project",
266+
project_id,
267+
"suppress",
268+
]);
269+
Ok(url)
270+
}
271+
272+
/// POST /organizations/<orgName>/groups/<groupName>/unsuppress.
273+
pub fn org_group_unsuppress(
274+
api_uri: &str,
275+
org_name: &str,
276+
group_name: &str,
277+
) -> Result<Url, BaseUriError> {
278+
let mut url = get_api_path(api_uri)?;
279+
url.path_segments_mut().unwrap().pop_if_empty().extend([
280+
"organizations",
281+
org_name,
282+
"groups",
283+
group_name,
284+
"unsuppress",
285+
]);
286+
Ok(url)
287+
}
288+
289+
/// POST /preferences/group/<groupName>/unsuppress.
290+
pub fn group_unsuppress(api_uri: &str, group_name: &str) -> Result<Url, BaseUriError> {
291+
let mut url = get_api_path(api_uri)?;
292+
url.path_segments_mut().unwrap().pop_if_empty().extend([
293+
"preferences",
294+
"group",
295+
group_name,
296+
"unsuppress",
297+
]);
298+
Ok(url)
299+
}
300+
301+
/// POST /preferences/project/<projectId>/unsuppress.
302+
pub fn project_unsuppress(api_uri: &str, project_id: &str) -> Result<Url, BaseUriError> {
303+
let mut url = get_api_path(api_uri)?;
304+
url.path_segments_mut().unwrap().pop_if_empty().extend([
305+
"preferences",
306+
"project",
307+
project_id,
308+
"unsuppress",
309+
]);
310+
Ok(url)
311+
}
312+
200313
/// GET /.well-known/openid-configuration
201314
pub fn oidc_discovery(api_uri: &str) -> Result<Url, BaseUriError> {
202315
Ok(get_api_path(api_uri)?.join(".well-known/openid-configuration")?)

cli/src/api/mod.rs

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ use crate::types::{
3232
FirewallLogFilter, FirewallLogResponse, FirewallPaginated, GetProjectResponse, HistoryJob,
3333
ListUserGroupsResponse, OrgGroupsResponse, OrgMembersResponse, OrgsResponse, PackageSpecifier,
3434
PackageSubmitResponse, Paginated, PingResponse, PolicyEvaluationRequest,
35-
PolicyEvaluationResponse, PolicyEvaluationResponseRaw, ProjectListEntry, RevokeTokenRequest,
36-
SubmitPackageRequest, UpdateProjectRequest, UserToken,
35+
PolicyEvaluationResponse, PolicyEvaluationResponseRaw, Preferences, ProjectListEntry,
36+
RevokeTokenRequest, SubmitPackageRequest, Suppression, UpdateProjectRequest, UserToken,
3737
};
3838

3939
pub mod endpoints;
@@ -596,6 +596,93 @@ impl PhylumApi {
596596
Ok(log)
597597
}
598598

599+
/// Get group preferences.
600+
pub async fn group_preferences(
601+
&self,
602+
org: Option<&str>,
603+
group: &str,
604+
) -> Result<Preferences<'static>> {
605+
match org {
606+
Some(org) => {
607+
let url =
608+
endpoints::org_group_preferences(&self.config.connection.uri, org, group)?;
609+
self.get(url).await
610+
},
611+
None => {
612+
#[derive(Deserialize)]
613+
struct Response<'a> {
614+
preferences: Preferences<'a>,
615+
}
616+
617+
let url = endpoints::group_preferences(&self.config.connection.uri, group)?;
618+
Ok(self.get::<Response, _>(url).await?.preferences)
619+
},
620+
}
621+
}
622+
623+
/// Get project preferences.
624+
pub async fn project_preferences(&self, project_id: &str) -> Result<Preferences<'static>> {
625+
#[derive(Deserialize)]
626+
struct Response<'a> {
627+
preferences: Preferences<'a>,
628+
}
629+
630+
let url = endpoints::project_preferences(&self.config.connection.uri, project_id)?;
631+
Ok(self.get::<Response, _>(url).await?.preferences)
632+
}
633+
634+
/// Add group suppression.
635+
pub async fn group_suppress(
636+
&self,
637+
org: Option<&str>,
638+
group: &str,
639+
suppressions: &[Suppression<'_>],
640+
) -> Result<()> {
641+
let url = match org {
642+
Some(org) => endpoints::org_group_suppress(&self.config.connection.uri, org, group)?,
643+
None => endpoints::group_suppress(&self.config.connection.uri, group)?,
644+
};
645+
self.send_request_raw(Method::POST, url, Some(suppressions)).await?;
646+
Ok(())
647+
}
648+
649+
/// Get project suppression.
650+
pub async fn project_suppress(
651+
&self,
652+
project_id: &str,
653+
suppressions: &[Suppression<'_>],
654+
) -> Result<()> {
655+
let url = endpoints::project_suppress(&self.config.connection.uri, project_id)?;
656+
self.send_request_raw(Method::POST, url, Some(suppressions)).await?;
657+
Ok(())
658+
}
659+
660+
/// Remove group suppression.
661+
pub async fn group_unsuppress(
662+
&self,
663+
org: Option<&str>,
664+
group: &str,
665+
unsuppressions: &[Suppression<'_>],
666+
) -> Result<()> {
667+
let url = match org {
668+
Some(org) => endpoints::org_group_unsuppress(&self.config.connection.uri, org, group)?,
669+
None => endpoints::group_unsuppress(&self.config.connection.uri, group)?,
670+
};
671+
self.send_request_raw(Method::POST, url, Some(unsuppressions)).await?;
672+
Ok(())
673+
}
674+
675+
/// Remove project suppression.
676+
pub async fn project_unsuppress(
677+
&self,
678+
project_id: &str,
679+
unsuppressions: &[Suppression<'_>],
680+
) -> Result<()> {
681+
let url = endpoints::project_unsuppress(&self.config.connection.uri, project_id)?;
682+
self.send_request_raw(Method::POST, url, Some(unsuppressions)).await?;
683+
Ok(())
684+
}
685+
599686
/// Get reachable vulnerabilities.
600687
#[cfg(feature = "vulnreach")]
601688
pub async fn vulnerabilities(&self, job: Job) -> Result<Vec<Vulnerability>> {

0 commit comments

Comments
 (0)