//! ref: composer/src/Composer/Command/AuditCommand.php
use anyhow::Result;
use indexmap::IndexMap;
use shirabe_external_packages::symfony::console::command::command::Command;
use shirabe_external_packages::symfony::console::input::InputInterface;
use shirabe_external_packages::symfony::console::output::OutputInterface;
use shirabe_php_shim::{
InvalidArgumentException, PhpMixed, UnexpectedValueException, array_fill_keys, array_merge,
implode, in_array,
};
use std::cell::RefCell;
use std::rc::Rc;
use crate::advisory::AuditConfig;
use crate::advisory::Auditor;
use crate::command::BaseCommand;
use crate::command::BaseCommandData;
use crate::command::base_command::base_command_initialize;
use crate::composer::PartialComposerHandle;
use crate::config::Config;
use crate::console::input::InputOption;
use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface;
use crate::io::IOInterface;
use crate::io::IOInterfaceImmutable;
use crate::repository::CanonicalPackagesTrait;
use crate::repository::InstalledRepository;
use crate::repository::RepositoryInterface;
use crate::repository::RepositorySet;
use crate::repository::RepositoryUtils;
#[derive(Debug)]
pub struct AuditCommand {
base_command_data: BaseCommandData,
}
impl Default for AuditCommand {
fn default() -> Self {
Self::new()
}
}
impl AuditCommand {
pub fn new() -> Self {
let command = AuditCommand {
base_command_data: BaseCommandData::new(None),
};
command
.configure()
.expect("AuditCommand::configure uses static, valid metadata");
command
}
}
impl Command for AuditCommand {
fn configure(&self) -> anyhow::Result<()> {
self.set_name("audit")?;
self.set_description("Checks for security vulnerability advisories for installed packages");
self.set_definition(&[
InputOption::new(
"no-dev",
None,
Some(InputOption::VALUE_NONE),
"Disables auditing of require-dev packages.",
None,
)
.unwrap()
.into(),
InputOption::new(
"format",
Some(PhpMixed::String("f".to_string())),
Some(InputOption::VALUE_REQUIRED),
"Output format. Must be \"table\", \"plain\", \"json\", or \"summary\".",
Some(PhpMixed::String(Auditor::FORMAT_TABLE.to_string())),
)
.unwrap()
.into(),
InputOption::new(
"locked",
None,
Some(InputOption::VALUE_NONE),
"Audit based on the lock file instead of the installed packages.",
None,
)
.unwrap()
.into(),
InputOption::new(
"abandoned",
None,
Some(InputOption::VALUE_REQUIRED),
"Behavior on abandoned packages. Must be \"ignore\", \"report\", or \"fail\".",
None,
)
.unwrap()
.into(),
InputOption::new(
"ignore-severity",
None,
Some(InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED),
"Ignore advisories of a certain severity level.",
Some(PhpMixed::Array(indexmap::IndexMap::new())),
)
.unwrap()
.into(),
InputOption::new(
"ignore-unreachable",
None,
Some(InputOption::VALUE_NONE),
"Ignore repositories that are unreachable or return a non-200 status code.",
None,
)
.unwrap()
.into(),
]);
self.set_help(
"The audit command checks for security vulnerability advisories for installed packages.\n\n\
If you do not want to include dev dependencies in the audit you can omit them with --no-dev\n\n\
If you want to ignore repositories that are unreachable or return a non-200 status code, use --ignore-unreachable\n\n\
Read more at https://getcomposer.org/doc/03-cli.md#audit"
);
Ok(())
}
fn execute(
&self,
input: Rc>,
_output: Rc>,
) -> anyhow::Result {
let composer = self.require_composer(None, None)?;
let packages = self.get_packages(&composer, input.clone())?;
if packages.is_empty() {
self.get_io().write_error("No packages - skipping audit.");
return Ok(0);
}
let composer = crate::command::composer_full(&composer);
let auditor = Auditor;
let mut repo_set = RepositorySet::new(
"stable",
indexmap::IndexMap::new(),
Vec::new(),
indexmap::IndexMap::new(),
indexmap::IndexMap::new(),
indexmap::IndexMap::new(),
);
for repo in composer
.get_repository_manager()
.borrow()
.get_repositories()
{
repo_set.add_repository(repo.clone())?;
}
let audit_config = AuditConfig::from_config(
&mut composer.get_config().borrow_mut(),
true,
Auditor::FORMAT_SUMMARY,
)?;
let abandoned = input
.borrow()
.get_option("abandoned")?
.as_string()
.map(|s| s.to_string());
if abandoned.is_some()
&& !in_array(
PhpMixed::String(abandoned.clone().unwrap()),
&PhpMixed::from(Auditor::ABANDONEDS.to_vec()),
true,
)
{
return Err(InvalidArgumentException {
message: format!(
"--abandoned must be one of {}.",
implode(
", ",
&Auditor::ABANDONEDS
.iter()
.map(|s| s.to_string())
.collect::>()
)
),
code: 0,
}
.into());
}
let abandoned = abandoned.unwrap_or_else(|| audit_config.audit_abandoned.clone());
let mut ignore_severities: indexmap::IndexMap> =
indexmap::IndexMap::new();
let cli_severities = input.borrow().get_option("ignore-severity")?;
if let Some(list) = cli_severities.as_list() {
for sev in list {
if let Some(s) = sev.as_string() {
ignore_severities.insert(s.to_string(), None);
}
}
}
for (k, v) in audit_config.ignore_severity_for_audit.clone() {
ignore_severities.insert(k, v);
}
let ignore_unreachable = input
.borrow()
.get_option("ignore-unreachable")?
.as_bool()
.unwrap_or(false)
|| audit_config.ignore_unreachable;
let audit_format = self.get_audit_format(input, "format")?;
Ok(auditor
.audit(
&mut *self.get_io().borrow_mut(),
&repo_set,
packages,
&audit_format,
false,
audit_config.ignore_list_for_audit.clone(),
&abandoned,
ignore_severities,
ignore_unreachable,
audit_config.ignore_abandoned_for_audit.clone(),
)?
.min(255))
}
fn initialize(
&self,
input: Rc>,
output: Rc>,
) -> anyhow::Result<()> {
base_command_initialize(self, input, output)
}
shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data);
}
impl BaseCommand for AuditCommand {
fn command_data(
&self,
) -> &shirabe_external_packages::symfony::console::command::command::CommandData {
self.base_command_data.command_data()
}
crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
}
impl AuditCommand {
fn get_packages(
&self,
composer: &PartialComposerHandle,
input: std::rc::Rc>,
) -> Result> {
let mut composer = crate::command::composer_full_mut(composer);
if input
.borrow()
.get_option("locked")?
.as_bool()
.unwrap_or(false)
{
let locker = composer.get_locker().clone();
let mut locker = locker.borrow_mut();
if !locker.is_locked() {
return Err(UnexpectedValueException {
message: "Valid composer.json and composer.lock files are required to run this command with --locked".to_string(),
code: 0,
}.into());
}
let locked_repo = locker.get_locked_repository(
!input
.borrow()
.get_option("no-dev")?
.as_bool()
.unwrap_or(false),
)?;
return locked_repo.borrow_mut().get_canonical_packages();
}
let _root_pkg = composer.get_package();
// TODO(phase-c): InstalledRepository::new expects Vec>, but
// get_local_repository returns &dyn InstalledRepositoryInterface. Conversion requires
// either cloning into a Box or restructuring InstalledRepository constructor.
let _ = RepositoryUtils::filter_required_packages;
todo!("audit get_packages non-locked branch needs installed-repo conversion")
}
}