feat: initial implementation of extension malware check (#4272)
Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com> Co-authored-by: Michael Neale <michael.neale@gmail.com> tested it a few ways, surgical change.
This commit is contained in:
@@ -0,0 +1,511 @@
|
|||||||
|
use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
|
||||||
|
use reqwest::Url;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::{debug, error, trace};
|
||||||
|
|
||||||
|
use crate::agents::extension::ExtensionError;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct OsvChecker {
|
||||||
|
client: reqwest::Client,
|
||||||
|
endpoint: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OsvChecker {
|
||||||
|
/// Constructs a checker. Honors OSV_ENDPOINT env var if present.
|
||||||
|
pub fn new() -> Result<Self, Box<ExtensionError>> {
|
||||||
|
let client = http_client().map_err(Box::new)?;
|
||||||
|
let endpoint = std::env::var("OSV_ENDPOINT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| Url::parse(&s).ok())
|
||||||
|
.unwrap_or_else(|| Url::parse(DEFAULT_OSV_ENDPOINT).expect("valid default OSV url"));
|
||||||
|
Ok(Self { client, endpoint })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs with a custom endpoint (handy for tests).
|
||||||
|
pub fn with_endpoint(endpoint: Url) -> Result<Self, Box<ExtensionError>> {
|
||||||
|
let client = http_client().map_err(Box::new)?;
|
||||||
|
Ok(Self { client, endpoint })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query OSV and **fail** if any MAL-* advisories are found.
|
||||||
|
/// - `ecosystem`: e.g., "npm", "PyPI"
|
||||||
|
/// - `version`: if `None`, checks by name only.
|
||||||
|
pub async fn deny_if_malicious(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
ecosystem: &str,
|
||||||
|
version: Option<&str>,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
deny_if_malicious_impl(&self.client, &self.endpoint, name, ecosystem, version).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience: infer ecosystem from command token + parse first package arg.
|
||||||
|
/// - ends_with("npx") → npm
|
||||||
|
/// - ends_with("uvx") → PyPI
|
||||||
|
/// unknown commands → skip (fail open)
|
||||||
|
pub async fn deny_if_malicious_cmd_args(cmd: &str, args: &[String]) -> Result<(), ExtensionError> {
|
||||||
|
let ecosystem = if cmd.ends_with("uvx") {
|
||||||
|
"PyPI"
|
||||||
|
} else if cmd.ends_with("npx") {
|
||||||
|
"npm"
|
||||||
|
} else {
|
||||||
|
debug!(%cmd, ?args, "Unknown ecosystem for command; skipping OSV check (fail open).");
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((name, version)) = parse_first_package_arg(ecosystem, args) {
|
||||||
|
OsvChecker::new()
|
||||||
|
.map_err(|e| *e)?
|
||||||
|
.deny_if_malicious(&name, ecosystem, version.as_deref())
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
debug!(%cmd, ?args, "No package token found; skipping OSV check.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Direct call without command inference.
|
||||||
|
pub async fn deny_if_malicious(
|
||||||
|
name: &str,
|
||||||
|
ecosystem: &str,
|
||||||
|
version: Option<&str>,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
OsvChecker::new()
|
||||||
|
.map_err(|e| *e)?
|
||||||
|
.deny_if_malicious(name, ecosystem, version)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_first_package_arg(ecosystem: &str, args: &[String]) -> Option<(String, Option<String>)> {
|
||||||
|
let is_flag = |s: &str| s.starts_with('-');
|
||||||
|
let token = args
|
||||||
|
.iter()
|
||||||
|
.find(|a| !is_flag(a.as_str()))?
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
if token.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match ecosystem {
|
||||||
|
"npm" => parse_npm_token(&token),
|
||||||
|
"PyPI" => parse_pypi_token(&token),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_npm_token(token: &str) -> Option<(String, Option<String>)> {
|
||||||
|
// Handles:
|
||||||
|
// react@18.3.1
|
||||||
|
// @scope/pkg@1.2.3 (split at the LAST '@')
|
||||||
|
// eslint (no version)
|
||||||
|
if token.starts_with('@') {
|
||||||
|
if let Some(idx) = token.rfind('@') {
|
||||||
|
if idx > 0 {
|
||||||
|
let (name, ver) = token.split_at(idx);
|
||||||
|
let ver = ver.trim_start_matches('@');
|
||||||
|
if !ver.is_empty() && ver != "latest" {
|
||||||
|
return Some((name.to_string(), Some(ver.to_string())));
|
||||||
|
} else {
|
||||||
|
return Some((name.to_string(), None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some((token.to_string(), None))
|
||||||
|
} else if let Some(idx) = token.find('@') {
|
||||||
|
let (name, ver) = token.split_at(idx);
|
||||||
|
let ver = ver.trim_start_matches('@');
|
||||||
|
if !name.is_empty() {
|
||||||
|
if !ver.is_empty() && ver != "latest" {
|
||||||
|
return Some((name.to_string(), Some(ver.to_string())));
|
||||||
|
} else {
|
||||||
|
return Some((name.to_string(), None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some((token.to_string(), None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_pypi_token(token: &str) -> Option<(String, Option<String>)> {
|
||||||
|
// Accept exact pins:
|
||||||
|
// package==1.2.3
|
||||||
|
// package[extra]==1.2.3
|
||||||
|
// Treat "latest" as None. Ignore other specifiers (>=, <=, ~=, !=) for pinning.
|
||||||
|
let lowered = token.to_ascii_lowercase();
|
||||||
|
if let Some(idx) = lowered.find("==") {
|
||||||
|
let (name, ver) = token.split_at(idx);
|
||||||
|
let ver = ver.trim_start_matches('=').trim_start_matches('=');
|
||||||
|
let name = name.trim();
|
||||||
|
if name.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if ver.is_empty() || ver.eq_ignore_ascii_case("latest") {
|
||||||
|
return Some((name.to_string(), None));
|
||||||
|
}
|
||||||
|
return Some((name.to_string(), Some(ver.to_string())));
|
||||||
|
}
|
||||||
|
Some((token.to_string(), None))
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_OSV_ENDPOINT: &str = "https://api.osv.dev/v1/query";
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct QueryReq<'a> {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
version: Option<&'a str>,
|
||||||
|
package: Package<'a>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
page_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Package<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
ecosystem: &'a str,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
purl: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct QueryResp {
|
||||||
|
#[serde(default)]
|
||||||
|
vulns: Vec<Vuln>,
|
||||||
|
#[serde(default)]
|
||||||
|
next_page_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Vuln {
|
||||||
|
id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
summary: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn deny_if_malicious_impl(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
endpoint: &Url,
|
||||||
|
name: &str,
|
||||||
|
ecosystem: &str,
|
||||||
|
version: Option<&str>,
|
||||||
|
) -> Result<(), ExtensionError> {
|
||||||
|
debug!(name, ecosystem, ?version, "OSV query starting");
|
||||||
|
let mut page_token: Option<String> = None;
|
||||||
|
let mut mal: Vec<Vuln> = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let body = QueryReq {
|
||||||
|
version,
|
||||||
|
package: Package {
|
||||||
|
name,
|
||||||
|
ecosystem,
|
||||||
|
purl: None,
|
||||||
|
},
|
||||||
|
page_token: page_token.clone(),
|
||||||
|
};
|
||||||
|
trace!(?body.page_token, "OSV page");
|
||||||
|
|
||||||
|
let resp = match client.post(endpoint.clone()).json(&body).send().await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
error!(%e, name, ecosystem, ?version, "OSV request failed; failing open.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = match resp.error_for_status() {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
error!(%e, name, ecosystem, ?version, "OSV HTTP error; failing open.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let payload: QueryResp = match resp.json().await {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
error!(%e, name, ecosystem, ?version, "OSV JSON parse error; failing open.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mal.extend(
|
||||||
|
payload
|
||||||
|
.vulns
|
||||||
|
.into_iter()
|
||||||
|
.filter(|v| v.id.starts_with("MAL-")),
|
||||||
|
);
|
||||||
|
|
||||||
|
match payload.next_page_token {
|
||||||
|
Some(tok) if !tok.is_empty() => page_token = Some(tok),
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mal.is_empty() {
|
||||||
|
let ver = version.unwrap_or("<any>");
|
||||||
|
let details = mal
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| {
|
||||||
|
if v.summary.is_empty() {
|
||||||
|
v.id
|
||||||
|
} else {
|
||||||
|
format!("{} — {}", v.id, v.summary)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("; ");
|
||||||
|
error!(name, ecosystem, version=%ver, %details, "Blocked malicious package via OSV MAL-*.");
|
||||||
|
return Err(ExtensionError::ConfigError(format!(
|
||||||
|
"Blocked malicious package: {name}@{ver} ({ecosystem}). OSV MAL advisories: {details}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(name, ecosystem, ?version, "OSV: no MAL advisories.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::result_large_err)]
|
||||||
|
fn http_client() -> Result<reqwest::Client, ExtensionError> {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
USER_AGENT,
|
||||||
|
HeaderValue::from_static("goose-osv-check/1.1 (+https://osv.dev)"),
|
||||||
|
);
|
||||||
|
reqwest::Client::builder()
|
||||||
|
.default_headers(headers)
|
||||||
|
.timeout(std::time::Duration::from_secs(10))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| ExtensionError::SetupError(format!("failed to build HTTP client: {e}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
use serial_test;
|
||||||
|
use tokio;
|
||||||
|
use wiremock::matchers::{method, path};
|
||||||
|
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||||
|
|
||||||
|
fn checker_for(server: &MockServer) -> OsvChecker {
|
||||||
|
let url = Url::parse(&format!("{}/v1/query", server.uri())).unwrap();
|
||||||
|
OsvChecker::with_endpoint(url).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to temporarily set an environment variable and restore it on drop
|
||||||
|
struct TempEnvVar {
|
||||||
|
key: String,
|
||||||
|
original: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TempEnvVar {
|
||||||
|
fn set(key: &str, value: &str) -> Self {
|
||||||
|
let original = std::env::var(key).ok();
|
||||||
|
std::env::set_var(key, value);
|
||||||
|
Self {
|
||||||
|
key: key.to_string(),
|
||||||
|
original,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TempEnvVar {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
match &self.original {
|
||||||
|
Some(val) => std::env::set_var(&self.key, val),
|
||||||
|
None => std::env::remove_var(&self.key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn allows_clean_package() {
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
Mock::given(method("POST"))
|
||||||
|
.and(path("/v1/query"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||||
|
"vulns": [],
|
||||||
|
"next_page_token": null
|
||||||
|
})))
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let c = checker_for(&server);
|
||||||
|
let res = c
|
||||||
|
.deny_if_malicious("some_clean_package", "PyPI", None)
|
||||||
|
.await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn blocks_malicious_package() {
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
Mock::given(method("POST"))
|
||||||
|
.and(path("/v1/query"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||||
|
"vulns": [ { "id": "MAL-1234", "summary": "Malicious package" } ],
|
||||||
|
"next_page_token": null
|
||||||
|
})))
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let c = checker_for(&server);
|
||||||
|
let res = c
|
||||||
|
.deny_if_malicious("bad_package", "PyPI", Some("1.0.0"))
|
||||||
|
.await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
let msg = format!("{:?}", res.unwrap_err());
|
||||||
|
assert!(msg.contains("Blocked malicious package"));
|
||||||
|
assert!(msg.contains("MAL-1234"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
|
async fn cmd_args_pypi_clean() {
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
Mock::given(method("POST"))
|
||||||
|
.and(path("/v1/query"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||||
|
"vulns": [],
|
||||||
|
"next_page_token": null
|
||||||
|
})))
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Use env var so OsvChecker::new() picks it up
|
||||||
|
let _env = TempEnvVar::set("OSV_ENDPOINT", &format!("{}/v1/query", server.uri()));
|
||||||
|
let args = vec!["some_clean_package==1.2.3".to_string()];
|
||||||
|
let res = deny_if_malicious_cmd_args("uvx", &args).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
|
async fn cmd_args_npm_scoped_malicious() {
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
Mock::given(method("POST"))
|
||||||
|
.and(path("/v1/query"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||||
|
"vulns": [ { "id": "MAL-9999", "summary": "Malicious npm package" } ],
|
||||||
|
"next_page_token": null
|
||||||
|
})))
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let _env = TempEnvVar::set("OSV_ENDPOINT", &format!("{}/v1/query", server.uri()));
|
||||||
|
let args = vec!["@scope/pkg@2.0.0".to_string()];
|
||||||
|
let res = deny_if_malicious_cmd_args("npx", &args).await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
let msg = format!("{:?}", res.unwrap_err());
|
||||||
|
assert!(msg.contains("Blocked malicious package"));
|
||||||
|
assert!(msg.contains("MAL-9999"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
|
async fn cmd_args_skip_flags_then_parse() {
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
Mock::given(method("POST"))
|
||||||
|
.and(path("/v1/query"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||||
|
"vulns": [],
|
||||||
|
"next_page_token": null
|
||||||
|
})))
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let _env = TempEnvVar::set("OSV_ENDPOINT", &format!("{}/v1/query", server.uri()));
|
||||||
|
let args = vec![
|
||||||
|
"--dry-run".into(),
|
||||||
|
"-y".into(),
|
||||||
|
"some_clean_package@1.2.3".into(),
|
||||||
|
];
|
||||||
|
let res = deny_if_malicious_cmd_args("npx", &args).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pagination_works() {
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
// 1st page: no vulns, but has next
|
||||||
|
Mock::given(method("POST"))
|
||||||
|
.and(path("/v1/query"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||||
|
"vulns": [],
|
||||||
|
"next_page_token": "page-2"
|
||||||
|
})))
|
||||||
|
.up_to_n_times(1)
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// 2nd page: MAL hit
|
||||||
|
Mock::given(method("POST"))
|
||||||
|
.and(path("/v1/query"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||||
|
"vulns": [ { "id": "MAL-4242", "summary": "Second page hit" } ],
|
||||||
|
"next_page_token": null
|
||||||
|
})))
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let c = checker_for(&server);
|
||||||
|
let res = c.deny_if_malicious("pkg", "npm", None).await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
let msg = format!("{:?}", res.unwrap_err());
|
||||||
|
assert!(msg.contains("MAL-4242"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn fail_open_on_http_error() {
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
Mock::given(method("POST"))
|
||||||
|
.and(path("/v1/query"))
|
||||||
|
.respond_with(ResponseTemplate::new(500))
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let c = checker_for(&server);
|
||||||
|
let res = c.deny_if_malicious("pkg", "npm", None).await;
|
||||||
|
assert!(res.is_ok(), "should fail-open on HTTP errors");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unknown_command_is_skipped() {
|
||||||
|
let args = vec!["whatever@1.0.0".into()];
|
||||||
|
// no mock server: we shouldn't call OSV at all
|
||||||
|
let res = deny_if_malicious_cmd_args("some-other-bin", &args).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_npm_scoped_with_version() {
|
||||||
|
assert_eq!(
|
||||||
|
super::parse_npm_token("@scope/pkg@1.2.3"),
|
||||||
|
Some(("@scope/pkg".into(), Some("1.2.3".into())))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_npm_unscoped_latest_is_none() {
|
||||||
|
assert_eq!(
|
||||||
|
super::parse_npm_token("react@latest"),
|
||||||
|
Some(("react".into(), None))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_pypi_exact_pin_and_latest() {
|
||||||
|
assert_eq!(
|
||||||
|
super::parse_pypi_token("requests==2.32.3"),
|
||||||
|
Some(("requests".into(), Some("2.32.3".into())))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
super::parse_pypi_token("requests==latest"),
|
||||||
|
Some(("requests".into(), None))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ use tracing::{error, warn};
|
|||||||
use super::extension::{ExtensionConfig, ExtensionError, ExtensionInfo, ExtensionResult, ToolInfo};
|
use super::extension::{ExtensionConfig, ExtensionError, ExtensionInfo, ExtensionResult, ToolInfo};
|
||||||
use super::tool_execution::ToolCallResult;
|
use super::tool_execution::ToolCallResult;
|
||||||
use crate::agents::extension::{Envs, ProcessExit};
|
use crate::agents::extension::{Envs, ProcessExit};
|
||||||
|
use crate::agents::extension_malware_check;
|
||||||
use crate::config::{Config, ExtensionConfigManager};
|
use crate::config::{Config, ExtensionConfigManager};
|
||||||
use crate::oauth::oauth_flow;
|
use crate::oauth::oauth_flow;
|
||||||
use crate::prompt_template;
|
use crate::prompt_template;
|
||||||
@@ -363,6 +364,10 @@ impl ExtensionManager {
|
|||||||
let command = Command::new(cmd).configure(|command| {
|
let command = Command::new(cmd).configure(|command| {
|
||||||
command.args(args).envs(all_envs);
|
command.args(args).envs(all_envs);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check for malicious packages before launching the process
|
||||||
|
extension_malware_check::deny_if_malicious_cmd_args(cmd, args).await?;
|
||||||
|
|
||||||
let client = child_process_client(command, timeout).await?;
|
let client = child_process_client(command, timeout).await?;
|
||||||
Box::new(client)
|
Box::new(client)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
mod agent;
|
mod agent;
|
||||||
mod context;
|
mod context;
|
||||||
pub mod extension;
|
pub mod extension;
|
||||||
|
pub mod extension_malware_check;
|
||||||
pub mod extension_manager;
|
pub mod extension_manager;
|
||||||
pub mod final_output_tool;
|
pub mod final_output_tool;
|
||||||
mod large_response_handler;
|
mod large_response_handler;
|
||||||
|
|||||||
Reference in New Issue
Block a user