Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added

- Support for C#'s `packages.*.config` lockfile type
- `phylum firewall log` command to browse firewall activity log

## 7.1.5 - 2024-11-26

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ phylum_lockfile = { path = "../lockfile", features = ["generator"] }
phylum_project = { path = "../phylum_project" }
phylum_types = { git = "https://github.com/phylum-dev/phylum-types", branch = "development" }
prettytable-rs = "0.10.0"
purl = "0.1.1"
rand = "0.8.4"
regex = "1.5.5"
reqwest = { version = "0.12.7", features = ["blocking", "json", "rustls-tls", "rustls-tls-native-roots", "rustls-tls-webpki-roots"], default-features = false }
Expand Down
13 changes: 13 additions & 0 deletions cli/src/api/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ pub fn org_groups_delete(
Ok(url)
}

/// Aviary activity endpoint.
pub fn firewall_log(api_uri: &str) -> Result<Url, BaseUriError> {
Ok(get_firewall_path(api_uri)?.join("activity")?)
}

/// GET /.well-known/openid-configuration
pub fn oidc_discovery(api_uri: &str) -> Result<Url, BaseUriError> {
Ok(get_api_path(api_uri)?.join(".well-known/openid-configuration")?)
Expand Down Expand Up @@ -242,6 +247,14 @@ fn get_locksmith_path(api_uri: &str) -> Result<Url, BaseUriError> {
Ok(parse_base_url(api_uri)?.join(LOCKSMITH_PATH)?)
}

fn get_firewall_path(api_uri: &str) -> Result<Url, BaseUriError> {
let mut api_path = parse_base_url(api_uri)?;
let host = api_path.host_str().ok_or(ParseError::EmptyHost)?;
let host = host.replacen("api.", "aviary.", 1);
api_path.set_host(Some(&host))?;
Ok(api_path)
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
44 changes: 39 additions & 5 deletions cli/src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::borrow::Cow;
use std::collections::HashSet;
use std::time::Duration;

use anyhow::{anyhow, Context};
use phylum_types::types::auth::AccessToken;
use phylum_types::types::common::{JobId, ProjectId};
use phylum_types::types::group::{
CreateGroupRequest, CreateGroupResponse, ListGroupMembersResponse,
Expand All @@ -27,10 +29,11 @@ use crate::auth::{
use crate::config::{AuthInfo, Config};
use crate::types::{
AddOrgUserRequest, AnalysisPackageDescriptor, ApiOrgGroup, CreateProjectRequest,
GetProjectResponse, HistoryJob, ListUserGroupsResponse, OrgGroupsResponse, OrgMembersResponse,
OrgsResponse, PackageSpecifier, PackageSubmitResponse, Paginated, PingResponse,
PolicyEvaluationRequest, PolicyEvaluationResponse, PolicyEvaluationResponseRaw,
ProjectListEntry, RevokeTokenRequest, SubmitPackageRequest, UpdateProjectRequest, UserToken,
FirewallLogFilter, FirewallLogResponse, FirewallPaginated, GetProjectResponse, HistoryJob,
ListUserGroupsResponse, OrgGroupsResponse, OrgMembersResponse, OrgsResponse, PackageSpecifier,
PackageSubmitResponse, Paginated, PingResponse, PolicyEvaluationRequest,
PolicyEvaluationResponse, PolicyEvaluationResponseRaw, ProjectListEntry, RevokeTokenRequest,
SubmitPackageRequest, UpdateProjectRequest, UserToken,
};

pub mod endpoints;
Expand All @@ -41,6 +44,7 @@ pub struct PhylumApi {
roles: HashSet<RealmRole>,
config: Config,
client: Client,
access_token: AccessToken,
}

/// Phylum Api Error type
Expand Down Expand Up @@ -140,7 +144,7 @@ impl PhylumApi {
// Try to parse token's roles.
let roles = jwt::user_roles(access_token.as_str()).unwrap_or_default();

Ok(Self { config, client, roles })
Ok(Self { config, client, roles, access_token })
}

async fn get<T: DeserializeOwned, U: IntoUrl>(&self, path: U) -> Result<T> {
Expand Down Expand Up @@ -562,6 +566,36 @@ impl PhylumApi {
Ok(())
}

/// Get Aviary activity log.
pub async fn firewall_log(
&self,
org: Option<&str>,
group: &str,
filter: FirewallLogFilter<'_>,
) -> Result<FirewallPaginated<FirewallLogResponse>> {
let user = match org {
Some(org) => Cow::Owned(format!("{org}/{group}")),
None => Cow::Borrowed(group),
};
let url = endpoints::firewall_log(&self.config.connection.uri)?;

let request =
self.client.get(url).basic_auth(user, Some(&self.access_token)).query(&filter);

let response = request.send().await?;
let status_code = response.status();
let body = response.text().await?;

if !status_code.is_success() {
let err = ResponseError { details: body, code: status_code };
return Err(err.into());
}

let log = serde_json::from_str(&body).map_err(|e| PhylumApiError::Other(e.into()))?;

Ok(log)
}

/// Get reachable vulnerabilities.
#[cfg(feature = "vulnreach")]
pub async fn vulnerabilities(&self, job: Job) -> Result<Vec<Vulnerability>> {
Expand Down
55 changes: 55 additions & 0 deletions cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,61 @@ pub fn add_subcommands(command: Command) -> Command {
.subcommand(
Command::new("unlink").about("Clear the configured default organization"),
),
)
.subcommand(
Command::new("firewall")
.about("Manage the package firewall")
.arg_required_else_help(true)
.subcommand_required(true)
.subcommand(
Command::new("log")
.about("Show firewall activity log")
.args(&[Arg::new("json")
.action(ArgAction::SetTrue)
.short('j')
.long("json")
.help("Produce output in json format (default: false)")])
.args(&[
Arg::new("group")
.value_name("GROUP_NAME")
.help("Specify a group to use for analysis")
.required(true),
Arg::new("ecosystem")
.long("ecosystem")
.value_name("ECOSYSTEM")
.help("Only show logs matching this ecosystem")
.value_parser([
"npm", "rubygems", "pypi", "maven", "nuget", "cargo",
]),
Arg::new("package")
.long("package")
.value_name("PURL")
.help("Only show logs matching this PURL")
.conflicts_with("ecosystem"),
Arg::new("action")
.long("action")
.value_name("ACTION")
.help("Only show logs matching this log action")
.value_parser([
"Download",
"AnalysisSuccess",
"AnalysisFailure",
"AnalysisWarning",
]),
Arg::new("before").long("before").value_name("TIMESTAMP").help(
"Only show logs created before this timestamp (RFC3339 format)",
),
Arg::new("after").long("after").value_name("TIMESTAMP").help(
"Only show logs created after this timestamp (RFC3339 format)",
),
Arg::new("limit")
.long("limit")
.value_name("COUNT")
.help("Maximum number of log entries to show")
.default_value("10")
.value_parser(1..=10_000),
]),
),
);

#[cfg(feature = "extensions")]
Expand Down
7 changes: 5 additions & 2 deletions cli/src/bin/phylum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use phylum_cli::commands::sandbox;
#[cfg(feature = "selfmanage")]
use phylum_cli::commands::uninstall;
use phylum_cli::commands::{
auth, find_dependency_files, group, init, jobs, org, packages, parse, project, status,
CommandResult, ExitCode,
auth, find_dependency_files, firewall, group, init, jobs, org, packages, parse, project,
status, CommandResult, ExitCode,
};
use phylum_cli::config::{self, Config};
use phylum_cli::spinner::Spinner;
Expand Down Expand Up @@ -145,6 +145,9 @@ async fn handle_commands() -> CommandResult {
"init" => init::handle_init(&Spinner::wrap(api).await?, sub_matches, config).await,
"status" => status::handle_status(sub_matches).await,
"org" => org::handle_org(&Spinner::wrap(api).await?, sub_matches, config).await,
"firewall" => {
firewall::handle_firewall(&Spinner::wrap(api).await?, sub_matches, config).await
},

#[cfg(feature = "selfmanage")]
"uninstall" => uninstall::handle_uninstall(sub_matches),
Expand Down
73 changes: 73 additions & 0 deletions cli/src/commands/firewall.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! Subcommand `phylum firewall`.

use std::borrow::Cow;
use std::str::FromStr;

use clap::ArgMatches;
use purl::Purl;

use crate::api::PhylumApi;
use crate::commands::{CommandResult, ExitCode};
use crate::config::Config;
use crate::format::Format;
use crate::print_user_failure;
use crate::types::FirewallLogFilter;

/// Handle `phylum firewall` subcommand.
pub async fn handle_firewall(
api: &PhylumApi,
matches: &ArgMatches,
config: Config,
) -> CommandResult {
match matches.subcommand() {
Some(("log", matches)) => handle_log(api, matches, config).await,
_ => unreachable!("invalid clap configuration"),
}
}

/// Handle `phylum firewall log` subcommand.
pub async fn handle_log(api: &PhylumApi, matches: &ArgMatches, config: Config) -> CommandResult {
let org = config.org();
let group = matches.get_one::<String>("group").unwrap();

// Get log filter args.
let ecosystem = matches.get_one::<String>("ecosystem");
let purl = matches.get_one::<String>("package");
let action = matches.get_one::<String>("action");
let before = matches.get_one::<String>("before");
let after = matches.get_one::<String>("after");
let limit = matches.get_one::<i64>("limit").unwrap();

// Parse PURL filter.
let parsed_purl = purl.map(|purl| Purl::from_str(purl));
let (ecosystem, namespace, name, version) = match &parsed_purl {
Some(Ok(purl)) => {
let ecosystem = Cow::Owned(purl.package_type().to_string());
(Some(ecosystem), purl.namespace(), Some(purl.name()), purl.version())
},
Some(Err(err)) => {
print_user_failure!("Could not parse purl {purl:?}: {err}");
return Ok(ExitCode::Generic);
},
None => (ecosystem.map(Cow::Borrowed), None, None, None),
};

// Construct the filter.
let filter = FirewallLogFilter {
ecosystem: ecosystem.as_ref().map(|e| e.as_str()),
namespace,
name,
version,
action: action.map(String::as_str),
before: before.map(String::as_str),
after: after.map(String::as_str),
limit: Some(*limit as i32),
};

let response = api.firewall_log(org, group, filter).await?;

let pretty = !matches.get_flag("json");
response.data.write_stdout(pretty);

Ok(ExitCode::Ok)
}
1 change: 1 addition & 0 deletions cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod auth;
#[cfg(feature = "extensions")]
pub mod extensions;
pub mod find_dependency_files;
pub mod firewall;
pub mod group;
pub mod init;
pub mod jobs;
Expand Down
Loading
Loading