diff options
| author | Mason Reed <mason@vector35.com> | 2025-08-26 23:59:38 -0400 |
|---|---|---|
| committer | Mason Reed <mason@vector35.com> | 2025-10-01 21:38:39 -0400 |
| commit | ede39aee7e00c40a43b67ca18dd8ab80ee863d85 (patch) | |
| tree | 67c5eda347ece2282e3c888f38066b496e06dec8 /plugins/warp/src | |
| parent | a1c46813e7f279aa4cfdb9dbb91c45b559ebeacd (diff) | |
[WARP] Enhanced network support
Diffstat (limited to 'plugins/warp/src')
| -rw-r--r-- | plugins/warp/src/container.rs | 90 | ||||
| -rw-r--r-- | plugins/warp/src/container/disk.rs | 57 | ||||
| -rw-r--r-- | plugins/warp/src/container/memory.rs | 15 | ||||
| -rw-r--r-- | plugins/warp/src/container/network.rs | 133 | ||||
| -rw-r--r-- | plugins/warp/src/container/network/client.rs | 405 | ||||
| -rw-r--r-- | plugins/warp/src/convert/types.rs | 11 | ||||
| -rw-r--r-- | plugins/warp/src/matcher.rs | 10 | ||||
| -rw-r--r-- | plugins/warp/src/plugin.rs | 84 | ||||
| -rw-r--r-- | plugins/warp/src/plugin/commit.rs | 150 | ||||
| -rw-r--r-- | plugins/warp/src/plugin/ffi.rs | 12 | ||||
| -rw-r--r-- | plugins/warp/src/plugin/ffi/container.rs | 271 | ||||
| -rw-r--r-- | plugins/warp/src/plugin/ffi/file.rs | 38 | ||||
| -rw-r--r-- | plugins/warp/src/plugin/ffi/function.rs | 2 | ||||
| -rw-r--r-- | plugins/warp/src/plugin/load.rs | 11 | ||||
| -rw-r--r-- | plugins/warp/src/plugin/settings.rs | 76 | ||||
| -rw-r--r-- | plugins/warp/src/plugin/workflow.rs | 10 |
16 files changed, 1262 insertions, 113 deletions
diff --git a/plugins/warp/src/container.rs b/plugins/warp/src/container.rs index 4cabc60e..8c76bc8f 100644 --- a/plugins/warp/src/container.rs +++ b/plugins/warp/src/container.rs @@ -1,5 +1,5 @@ use crate::container::disk::NAMESPACE_DISK_SOURCE; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt::{Debug, Display}; use std::hash::Hash; use std::io; @@ -10,6 +10,7 @@ use uuid::Uuid; use warp::r#type::guid::TypeGUID; use warp::r#type::{ComputedType, Type}; use warp::signature::function::{Function, FunctionGUID}; +use warp::symbol::Symbol; use warp::target::Target; pub mod disk; @@ -123,6 +124,77 @@ impl Display for SourcePath { } } +/// A tag associated with a source in a container. +/// +/// Tags can be used to categorize and filter sources when querying the container. +pub type SourceTag = compact_str::CompactString; + +/// A search query for finding items in a container. +/// +/// This struct represents a search request that can be used to find functions, types and any other +/// items associated with the container. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ContainerSearchQuery { + /// The search query string to match against items. + pub query: String, + /// Optional offset into the results for pagination. + pub offset: Option<usize>, + /// Optional maximum number of results to return. + pub limit: Option<usize>, + /// Optional source ID to restrict the search to. + pub source: Option<SourceId>, + /// Optional list of tags to restrict the search to. + pub tags: Vec<SourceTag>, + // TODO: Add field for function guid? conceivable someone wants to filter through those. +} + +impl ContainerSearchQuery { + pub fn new(query: String) -> Self { + Self { + query, + offset: None, + limit: None, + source: None, + tags: Vec::new(), + } + } +} + +/// An item returned from a container search. +/// +/// Contains the source ID where the item was found and the specific kind of item. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ContainerSearchItem { + /// The source ID where this item was found + pub source: SourceId, + /// The specific kind of item that was found + pub kind: ContainerSearchItemKind, +} + +/// The kind of item found in a container search. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ContainerSearchItemKind { + /// A source identified by its ID + Source { path: SourcePath, id: SourceId }, + /// A function definition + Function(Function), + /// A type definition + Type(Type), + /// A symbol definition + Symbol(Symbol), +} + +/// Response containing the results of a container search. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ContainerSearchResponse { + /// The matching items found in the search + pub items: Vec<ContainerSearchItem>, + /// Total number of matching items available + pub total: usize, + /// Starting offset of these results in the total set + pub offset: usize, +} + /// Storage for WARP information. /// /// Containers are made up of sources, see [`SourceId`] for more details. @@ -170,6 +242,9 @@ pub trait Container: Send + Sync + Display + Debug { /// that a source has uncommitted changes. fn is_source_uncommitted(&self, source: &SourceId) -> ContainerResult<bool>; + /// Retrieves the set of [`SourceTag`] for the given source. + fn source_tags(&self, source: &SourceId) -> ContainerResult<HashSet<SourceTag>>; + /// Retrieve the [`SourcePath`] for the given source. /// /// NOTE: This does not have to be a filesystem path, its representation is dictated @@ -216,6 +291,7 @@ pub trait Container: Send + Sync + Display + Debug { fn fetch_functions( &mut self, _target: &Target, + _tags: &[SourceTag], _functions: &[FunctionGUID], ) -> ContainerResult<()> { Ok(()) @@ -250,7 +326,6 @@ pub trait Container: Send + Sync + Display + Debug { guid: &FunctionGUID, ) -> ContainerResult<Vec<SourceId>>; - // TODO: Allocating with Vec is not good. /// Plural version of [`Container::sources_with_function_guid`]. /// /// Each source will have a list of the containing GUID's so that when looking up a source you give @@ -276,4 +351,15 @@ pub trait Container: Send + Sync + Display + Debug { ) -> ContainerResult<bool> { Ok(!self.functions_with_guid(target, source, guid)?.is_empty()) } + + /// Perform a paginated search over the container contents. + /// + /// The container implementation is responsible for interpreting [`ContainerSearchQuery::query`] + /// for example, locally you may not have the capabilities to perform a sane fuzzy search, so the query + /// is exact, whereas a database-backed container may opt to instead perform a fuzzy search. + /// + /// NOTE: This is intended for user-performed actions, as the query may look up over the network. + fn search(&self, _query: &ContainerSearchQuery) -> ContainerResult<ContainerSearchResponse> { + Ok(ContainerSearchResponse::default()) + } } diff --git a/plugins/warp/src/container/disk.rs b/plugins/warp/src/container/disk.rs index ca685a5f..02da0f30 100644 --- a/plugins/warp/src/container/disk.rs +++ b/plugins/warp/src/container/disk.rs @@ -1,5 +1,7 @@ -use crate::container::{Container, ContainerError, ContainerResult, SourceId, SourcePath}; -use std::collections::HashMap; +use crate::container::{ + Container, ContainerError, ContainerResult, SourceId, SourcePath, SourceTag, +}; +use std::collections::{HashMap, HashSet}; use std::fmt::{Debug, Display, Formatter}; use std::hash::{Hash, Hasher}; use std::path::PathBuf; @@ -20,11 +22,12 @@ pub const NAMESPACE_DISK_SOURCE: Uuid = uuid!("ea89e8ab-a27a-432b-8fbd-77b026cd5 pub struct DiskContainer { pub name: String, pub sources: HashMap<SourceId, DiskContainerSource>, + pub writable: bool, } impl DiskContainer { pub fn new(name: String, sources: HashMap<SourceId, DiskContainerSource>) -> Self { - Self { name, sources } + Self { name, sources, writable: true } } pub fn new_from_dir(dir_path: PathBuf) -> Self { @@ -56,6 +59,22 @@ impl DiskContainer { Self::new(name, sources) } + + pub fn insert_source(&mut self, id: SourceId, path: SourcePath) -> ContainerResult<()> { + if !self.writable || self.sources.contains_key(&id) { + return Err(ContainerError::SourceAlreadyExists(path)); + } + // NOTE: We let anyone add a file from anywhere on the file system because of this. + let disk_source = match path.0.exists() { + true => DiskContainerSource::new_from_path(path.clone())?, + false => { + let file = WarpFile::new(WarpFileHeader::new(), vec![]); + DiskContainerSource::new(path, file) + } + }; + self.sources.insert(id, disk_source); + Ok(()) + } } impl Container for DiskContainer { @@ -66,23 +85,8 @@ impl Container for DiskContainer { fn add_source(&mut self, path: SourcePath) -> ContainerResult<SourceId> { // Disk sources have there source id computed from the path. let source_id = path.to_source_id(); - if self.sources.contains_key(&source_id) { - return Err(ContainerError::SourceAlreadyExists(path)); - } - // NOTE: We let anyone add a file from anywhere on the file system because of this. - match path.0.exists() { - true => { - let disk_source = DiskContainerSource::new_from_path(path.clone())?; - self.sources.insert(source_id, disk_source); - Ok(source_id) - } - false => { - let file = WarpFile::new(WarpFileHeader::new(), vec![]); - let disk_source = DiskContainerSource::new(path, file); - self.sources.insert(source_id, disk_source); - Ok(source_id) - } - } + self.insert_source(source_id, path)?; + Ok(source_id) } fn commit_source(&mut self, source: &SourceId) -> ContainerResult<bool> { @@ -99,8 +103,7 @@ impl Container for DiskContainer { .sources .get(source) .ok_or(ContainerError::SourceNotFound(*source))?; - // TODO: I think this should be up to the container. (cant write to bundled files) - Ok(true) + Ok(self.writable) } fn is_source_uncommitted(&self, source: &SourceId) -> ContainerResult<bool> { @@ -111,6 +114,14 @@ impl Container for DiskContainer { Ok(disk_source.uncommitted) } + fn source_tags(&self, source: &SourceId) -> ContainerResult<HashSet<SourceTag>> { + let disk_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + Ok(disk_source.tags.clone()) + } + fn source_path(&self, source: &SourceId) -> ContainerResult<SourcePath> { let disk_source = self .sources @@ -277,6 +288,7 @@ impl Debug for DiskContainer { pub struct DiskContainerSource { pub path: SourcePath, + pub tags: HashSet<SourceTag>, file: WarpFile<'static>, uncommitted: bool, } @@ -285,6 +297,7 @@ impl DiskContainerSource { pub fn new(path: SourcePath, file: WarpFile<'static>) -> Self { Self { path, + tags: HashSet::new(), file, uncommitted: false, } diff --git a/plugins/warp/src/container/memory.rs b/plugins/warp/src/container/memory.rs index cf53390e..51628d6c 100644 --- a/plugins/warp/src/container/memory.rs +++ b/plugins/warp/src/container/memory.rs @@ -1,5 +1,7 @@ -use crate::container::{Container, ContainerError, ContainerResult, SourceId, SourcePath}; -use std::collections::HashMap; +use crate::container::{ + Container, ContainerError, ContainerResult, SourceId, SourcePath, SourceTag, +}; +use std::collections::{HashMap, HashSet}; use std::fmt::Display; use warp::r#type::guid::TypeGUID; use warp::r#type::{ComputedType, Type}; @@ -73,6 +75,15 @@ impl Container for MemoryContainer { Ok(false) } + fn source_tags(&self, source: &SourceId) -> ContainerResult<HashSet<SourceTag>> { + let _memory_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + // NOTE: Memory containers do not have a notion of tags. + Ok(HashSet::default()) + } + fn source_path(&self, source: &SourceId) -> ContainerResult<SourcePath> { Err(ContainerError::SourcePathUnavailable(*source)) } diff --git a/plugins/warp/src/container/network.rs b/plugins/warp/src/container/network.rs index 2a2d7c65..11f4db6b 100644 --- a/plugins/warp/src/container/network.rs +++ b/plugins/warp/src/container/network.rs @@ -1,7 +1,12 @@ use crate::container::disk::DiskContainer; -use crate::container::{Container, ContainerError, ContainerResult, SourceId, SourcePath}; -use std::collections::HashMap; +use crate::container::{ + Container, ContainerError, ContainerResult, ContainerSearchQuery, ContainerSearchResponse, + SourceId, SourcePath, SourceTag, +}; +use directories::ProjectDirs; +use std::collections::{HashMap, HashSet}; use std::fmt::{Debug, Display, Formatter}; +use std::path::PathBuf; use warp::chunk::{Chunk, ChunkKind, CompressionType}; use warp::r#type::guid::TypeGUID; use warp::r#type::{ComputedType, Type}; @@ -12,6 +17,7 @@ use warp::{WarpFile, WarpFileHeader}; pub mod client; +use crate::container::ContainerError::CannotCreateSource; pub use client::NetworkClient; /// This is the id on the server for the [`Target`], we can get it via [`NetworkClient::query_target_id`]. @@ -22,23 +28,42 @@ pub struct NetworkContainer { /// This is the store that the interface will write to; then we have special functions for pulling /// and pushing to the network source. cache: DiskContainer, + /// Where to place newly created sources. + /// + /// This is typically a directory inside [`NetworkContainer::root_cache_location`]. + cache_path: PathBuf, /// Populated when targets are queried. known_targets: HashMap<Target, Option<NetworkTargetId>>, - /// Populated with function sources are queried. + /// Populated when function sources are queried. known_function_sources: HashMap<FunctionGUID, Vec<SourceId>>, /// Populated when user adds function, this is used for writing back to the server. added_chunks: HashMap<SourceId, Vec<Chunk<'static>>>, + /// Populated when connecting to the server, this is used to determine which sources are writable. + /// + /// NOTE: This is only populated when logged in, as guest users do not have write permissions. + writable_sources: HashSet<SourceId>, } impl NetworkContainer { - pub fn new(client: NetworkClient) -> Self { - Self { - cache: DiskContainer::new("Network Container".to_string(), HashMap::new()), + pub fn new(client: NetworkClient, cache_path: PathBuf, writable_sources: &[SourceId]) -> Self { + let mut container = Self { + cache: DiskContainer::new_from_dir(cache_path.clone()), + cache_path, client, known_targets: HashMap::new(), known_function_sources: HashMap::new(), added_chunks: HashMap::new(), + writable_sources: writable_sources.into_iter().copied().collect(), + }; + + // TODO: Because of this little hack, methinks we should move writable sources to after the + // TODO: container is actually created, but before it is moved into the global container cache. + // Probe all writable sources, so the container knows about them properly. + for source in writable_sources { + container.probe_source(*source); } + + container } /// Gets the network id for the `target`, this will be used in later function queries. @@ -72,6 +97,7 @@ impl NetworkContainer { pub fn get_unseen_functions_source( &mut self, target: Option<&Target>, + tags: &[SourceTag], guids: &[FunctionGUID], ) -> HashMap<SourceId, Vec<FunctionGUID>> { let Some(target_id) = target.and_then(|t| self.get_target_id(t)) else { @@ -88,9 +114,9 @@ impl NetworkContainer { let mut result: HashMap<SourceId, Vec<FunctionGUID>> = HashMap::new(); // Only query server for unknown guids if we have any. if !unknown.is_empty() { - if let Some(queried_results) = self - .client - .query_functions_source(Some(target_id), &unknown) + if let Some(queried_results) = + self.client + .query_functions_source(Some(target_id), tags, &unknown) { // Cache the new results, this means we will not try and contact the server for that guids source. // NOTE: Here we do not just simply list the queried results because we also @@ -139,6 +165,8 @@ impl NetworkContainer { match &chunk.kind { ChunkKind::Signature(sc) => { let functions: Vec<_> = sc.functions().collect(); + // Probe the source before attempting to access it, as it might not exist locally. + self.probe_source(*source); match self.cache.add_functions(target, source, &functions) { Ok(_) => log::debug!( "Added {} functions into cached source '{}'", @@ -164,7 +192,46 @@ impl NetworkContainer { /// /// **This is blocking** pub fn push_file(&mut self, source_id: SourceId, file: &WarpFile) { - self.client.push_file(source_id, file); + // TODO: We need a better name for the commit. I would like to derive it automatically from + // TODO: something instead of having the user give it TBH. + self.client.push_file(source_id, file, "commit"); + } + + /// Probe the source to make sure it exists in the cache. Retrieving the name from the server. + /// + /// **This is blocking** + pub fn probe_source(&mut self, source_id: SourceId) { + if !self.cache.source_path(&source_id).is_ok() { + // Add the source to the cache. Using the source id and source name as the source path. + match self.client.source_name(source_id) { + Ok(source_name) => { + // To prevent two sources with the same name colliding, we add the source id to the source name. + let source_path = self + .cache_path + .join(source_id.to_string()) + .join(source_name); + let _ = self.cache.insert_source(source_id, SourcePath(source_path)); + } + Err(e) => { + log::error!("Failed to probe source '{}': {}", source_id, e); + } + } + } + } + + pub fn root_cache_location() -> PathBuf { + // - Windows: %LOCALAPPDATA%\<org>\<app>\cache + // - macOS: ~/Library/Caches/<org>.<app> + // - Linux: $XDG_CACHE_HOME/<app> or ~/.cache/<app> + if let Some(proj_dirs) = ProjectDirs::from("", "Vector35", "Binary Ninja") { + proj_dirs.cache_dir().to_path_buf() + } else { + // Fallback if OS dirs cannot be determined + std::env::current_dir() + .unwrap_or_else(|_| PathBuf::from(".")) + .join(".cache") + .join("binaryninja") + } } } @@ -174,9 +241,16 @@ impl Container for NetworkContainer { } fn add_source(&mut self, path: SourcePath) -> ContainerResult<SourceId> { - // TODO: How do we want to let users create new sources? - log::error!("NetworkContainer::add_source not allowed"); - Err(ContainerError::CannotCreateSource(path)) + // Send a **blocking** request to the server to create the source. + // NOTE: The user must be logged in for this to work. + // TODO: Some better error handling to alert the user that they are not logged in / creating existing sources. + let source = self + .client + .create_source(&path.to_string()) + .map_err(|_| CannotCreateSource(path))?; + // Must probe the source before attempting to access it, as it does not exist locally. + self.probe_source(source); + Ok(source) } fn commit_source(&mut self, source: &SourceId) -> ContainerResult<bool> { @@ -184,21 +258,26 @@ impl Container for NetworkContainer { .added_chunks .remove(source) .ok_or(ContainerError::SourceNotFound(source.clone()))?; - let file = WarpFile::new(WarpFileHeader::new(), chunks); + // Because each add operation is its own chunk, we should merge them into larger chunks before sending. + let merged_chunks = Chunk::merge(&chunks, CompressionType::Zstd); + let file = WarpFile::new(WarpFileHeader::new(), merged_chunks); self.push_file(*source, &file); Ok(true) } fn is_source_writable(&self, source: &SourceId) -> ContainerResult<bool> { - // TODO: This is retrievable from /users/me/sources we will grab it when connecting. - log::error!("NetworkContainer::is_source_writable not allowed"); - Err(ContainerError::SourceNotWritable(source.clone())) + // Assume that all writable_sources are also in the cache (through `probe_source`). + Ok(self.writable_sources.contains(source)) } fn is_source_uncommitted(&self, source: &SourceId) -> ContainerResult<bool> { Ok(self.added_chunks.contains_key(source)) } + fn source_tags(&self, source: &SourceId) -> ContainerResult<HashSet<SourceTag>> { + self.cache.source_tags(source) + } + fn source_path(&self, source: &SourceId) -> ContainerResult<SourcePath> { self.cache.source_path(source) } @@ -246,10 +325,12 @@ impl Container for NetworkContainer { fn fetch_functions( &mut self, target: &Target, + tags: &[SourceTag], functions: &[FunctionGUID], ) -> ContainerResult<()> { // NOTE: Blocking request to get the mapped function sources. - let mapped_unseen_functions = self.get_unseen_functions_source(Some(&target), functions); + let mapped_unseen_functions = + self.get_unseen_functions_source(Some(&target), tags, functions); // Actually get the function data for the unseen guids, we really only want to do this once per // session, anymore, and this is annoying! @@ -308,16 +389,28 @@ impl Container for NetworkContainer { ) -> ContainerResult<Vec<Function>> { self.cache.functions_with_guid(target, source, guid) } + + fn search(&self, query: &ContainerSearchQuery) -> ContainerResult<ContainerSearchResponse> { + // TODO: Give this an actual network error. + self.client + .search(query) + .ok_or(ContainerError::CorruptedData( + "search query failed to validate", + )) + } } impl Debug for NetworkContainer { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("NetworkContainer").finish() + f.debug_struct("NetworkContainer") + .field("client", &self.client) + .field("cache_path", &self.cache_path) + .finish() } } impl Display for NetworkContainer { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("NetworkContainer").finish() + Display::fmt(&self.client.server_url, f) } } diff --git a/plugins/warp/src/container/network/client.rs b/plugins/warp/src/container/network/client.rs index f77f1118..39e7640a 100644 --- a/plugins/warp/src/container/network/client.rs +++ b/plugins/warp/src/container/network/client.rs @@ -1,12 +1,20 @@ use crate::container::network::NetworkTargetId; -use crate::container::SourceId; +use crate::container::{ + ContainerSearchItem, ContainerSearchItemKind, ContainerSearchQuery, ContainerSearchResponse, + SourceId, SourcePath, SourceTag, +}; use reqwest::blocking::Client; use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; use reqwest::StatusCode; +use serde::Deserialize; use serde_json::json; use std::collections::HashMap; use std::str::FromStr; -use warp::signature::function::FunctionGUID; +use uuid::Uuid; +use warp::chunk::ChunkKind; +use warp::r#type::guid::TypeGUID; +use warp::r#type::{ComputedType, Type}; +use warp::signature::function::{Function, FunctionGUID}; use warp::target::Target; use warp::WarpFile; @@ -16,7 +24,7 @@ use warp::WarpFile; #[derive(Clone, Debug)] pub struct NetworkClient { client: Client, - server_url: String, + pub server_url: String, } impl NetworkClient { @@ -65,6 +73,116 @@ impl NetworkClient { Ok(resp.status()) } + /// Query the logged in user. + /// + /// NOTE: **THIS IS BLOCKING** + /// + /// Route: `api/v1/users/me` (TODO: Comment about the query) + pub fn current_user(&self) -> reqwest::Result<(i32, String)> { + let current_user_url = format!("{}/api/v1/users/me", self.server_url); + + #[derive(Deserialize)] + struct CurrentUser { + username: String, + id: i32, + } + + let resp = self + .client + .get(¤t_user_url) + .send()? + .error_for_status()?; + let user: CurrentUser = resp.json()?; + Ok((user.id, user.username)) + } + + /// Query the logged in user. + /// + /// NOTE: **THIS IS BLOCKING** + /// + /// Route: `api/v1/users/me` (TODO: Comment about the query) + pub fn source_name(&self, id: SourceId) -> reqwest::Result<String> { + let source_url = format!("{}/api/v1/sources/{}", self.server_url, id); + + #[derive(Deserialize)] + struct Source { + name: String, + } + + let resp = self.client.get(&source_url).send()?.error_for_status()?; + let src: Source = resp.json()?; + Ok(src.name) + } + + /// Create a new source with the given name. + /// + /// The current user will be added to the source. + /// + /// NOTE: You must be logged in to create a source. + /// + /// NOTE: **THIS IS BLOCKING** + /// + /// Route: `api/v1/sources/` + pub fn create_source(&self, name: &str) -> reqwest::Result<SourceId> { + let source_url = format!("{}/api/v1/sources", self.server_url); + + let body = json!({ + "name": name, + // Passing nothing here will add the current user to the source. + "user_ids": [] + }); + + #[derive(Deserialize)] + struct CreateSourceResponse { + id: Uuid, + } + + let resp = self + .client + .post(&source_url) + .json(&body) + .send()? + .error_for_status()?; + + let parsed: CreateSourceResponse = resp.json()?; + Ok(SourceId(parsed.id)) + } + + /// Query the [`SourceId`]s for the given user. + /// + /// NOTE: **THIS IS BLOCKING** + /// + /// Route: `api/v1/sources/query` (TODO: Comment about the query) + pub fn query_sources(&self, user_id: Option<i32>) -> reqwest::Result<Vec<SourceId>> { + let sources_url = format!("{}/api/v1/sources/query", self.server_url); + + #[derive(Deserialize)] + struct SourceItem { + id: Uuid, + } + + #[derive(Deserialize)] + struct SourcesQueryResponse { + items: Vec<SourceItem>, + } + + let mut query = HashMap::new(); + if let Some(user_id) = user_id { + query.insert("user_id", user_id); + } + let query_str = json!(query).to_string(); + let resp = self + .client + .post(&sources_url) + .body(query_str) + .header("Content-Type", "application/json") + .send()? + .error_for_status()?; + + let parsed: SourcesQueryResponse = resp.json()?; + Ok(parsed.items.into_iter().map(|it| SourceId(it.id)).collect()) + } + /// Query the [`NetworkTargetId`] for the given [`Target`]. /// /// NOTE: **THIS IS BLOCKING** @@ -78,25 +196,36 @@ impl NetworkClient { query.insert("platform", platform); } if let Some(architecture) = &target.architecture { - query.insert("architecture", architecture); + query.insert("arch", architecture); + } + let query_str = json!(query).to_string(); + + #[derive(Deserialize)] + struct TargetQueryResponse { + id: NetworkTargetId, } // NOTE: This is blocking. - let target_id: NetworkTargetId = self + let response = self .client - .get(query_target_url) - .query(&query) + .post(query_target_url) + .body(query_str) + .header("Content-Type", "application/json") .send() - .ok()? - .json::<NetworkTargetId>() .ok()?; - Some(target_id) + // Assuming the first response is the one we want. + // TODO: Handle multiple responses, or error out. + let json_response: Vec<TargetQueryResponse> = response.json().ok()?; + let first_response = json_response.first()?; + + Some(first_response.id) } fn query_functions_body( target: Option<NetworkTargetId>, source: Option<SourceId>, + source_tags: &[SourceTag], guids: &[FunctionGUID], ) -> serde_json::Value { let guids_str: Vec<String> = guids.iter().map(|g| g.to_string()).collect(); @@ -112,6 +241,9 @@ impl NetworkClient { if let Some(source_id) = source { body["source_id"] = json!(source_id.to_string()); } + if !source_tags.is_empty() { + body["source_tags"] = json!(source_tags); + } body } @@ -127,7 +259,9 @@ impl NetworkClient { guids: &[FunctionGUID], ) -> Option<WarpFile<'static>> { let query_functions_url = format!("{}/api/v1/functions/query", self.server_url); - let payload = Self::query_functions_body(target, source, guids); + // TODO: Allow for source tags? We really only need this in query_functions_source as that + // TODO: is what prevents a undesired source from being "known" to the container. + let payload = Self::query_functions_body(target, source, &[], guids); // Make the POST request let response = self @@ -154,11 +288,12 @@ impl NetworkClient { pub fn query_functions_source( &self, target: Option<NetworkTargetId>, + tags: &[SourceTag], guids: &[FunctionGUID], ) -> Option<HashMap<SourceId, Vec<FunctionGUID>>> { let query_functions_source_url = format!("{}/api/v1/functions/query/source", self.server_url); - let payload = Self::query_functions_body(target, None, guids); + let payload = Self::query_functions_body(target, None, tags, guids); // Make the POST request let response = self @@ -194,20 +329,24 @@ impl NetworkClient { /// NOTE: **THIS IS BLOCKING** /// /// Route: `api/v1/files/{source}` - pub fn push_file(&self, source_id: SourceId, file: &WarpFile) -> bool { - let push_file_url = format!("{}/api/v1/files/{}", self.server_url, source_id.to_string()); + pub fn push_file(&self, source_id: SourceId, file: &WarpFile, name: &str) -> bool { + let push_file_url = format!("{}/api/v1/files", self.server_url); // Convert WarpFile to bytes let file_bytes = file.to_bytes(); - // Create the form part with the file - let form = reqwest::blocking::multipart::Form::new().part( - "file", - reqwest::blocking::multipart::Part::bytes(file_bytes) - .file_name("data.warp") - .mime_str("application/octet-stream") - .unwrap(), - ); + let Ok(file_part) = reqwest::blocking::multipart::Part::bytes(file_bytes) + .file_name("data.warp") + .mime_str("application/octet-stream") + else { + log::error!("Failed to create file part"); + return false; + }; + + let form = reqwest::blocking::multipart::Form::new() + .part("file", file_part) + .text("name", name.to_string()) + .text("source", source_id.to_string()); // Send the request match self.client.post(&push_file_url).multipart(form).send() { @@ -225,4 +364,226 @@ impl NetworkClient { } } } + + pub fn function_data(&self, id: i32) -> Option<Function> { + let function_data_url = format!("{}/api/v1/functions/{}/data", self.server_url, id); + let response = self.client.get(&function_data_url).send().ok()?; + if !response.status().is_success() { + log::error!( + "Failed to fetch function data for {}: {}", + id, + response.status() + ); + return None; + } + let bytes = response.bytes().ok()?; + Function::from_bytes(bytes.as_ref()) + } + + pub fn function_datas(&self, ids: &[i32]) -> Option<Vec<Function>> { + if ids.is_empty() { + return Some(Vec::new()); + } + let function_data_url = format!("{}/api/v1/functions/data", self.server_url); + let body = json!({ + "ids": ids, + }); + let response = self + .client + .post(&function_data_url) + .json(&body) + .send() + .ok()?; + if !response.status().is_success() { + log::error!("Failed to fetch function data: {}", response.status()); + return None; + } + let bytes = response.bytes().ok()?; + let file = WarpFile::from_bytes(bytes.as_ref())?; + let mut functions = Vec::with_capacity(ids.len()); + for chunk in file.chunks { + let ChunkKind::Signature(sc) = chunk.kind else { + continue; + }; + functions.extend(sc.functions()); + } + Some(functions) + } + + pub fn type_data(&self, guid: TypeGUID) -> Option<Type> { + let type_data_url = format!("{}/api/v1/types/{}/data", self.server_url, guid.to_string()); + let response = self.client.get(&type_data_url).send().ok()?; + if !response.status().is_success() { + log::error!( + "Failed to fetch type data for {}: {}", + guid.to_string(), + response.status() + ); + return None; + } + let bytes = response.bytes().ok()?; + Type::from_bytes(bytes.as_ref()) + } + + pub fn type_datas(&self, guids: &[TypeGUID]) -> Option<Vec<ComputedType>> { + if guids.is_empty() { + return Some(Vec::new()); + } + let type_data_url = format!("{}/api/v1/types/data", self.server_url); + let body = json!({ + "ids": guids.iter().map(|g| g.to_string()).collect::<Vec<_>>(), + }); + let response = self.client.post(&type_data_url).json(&body).send().ok()?; + if !response.status().is_success() { + log::error!("Failed to fetch type data: {}", response.status()); + return None; + } + let bytes = response.bytes().ok()?; + let file = WarpFile::from_bytes(bytes.as_ref())?; + let mut types = Vec::with_capacity(guids.len()); + for chunk in file.chunks { + let ChunkKind::Type(tc) = chunk.kind else { + continue; + }; + types.extend(tc.types()); + } + Some(types) + } + + pub fn search(&self, query: &ContainerSearchQuery) -> Option<ContainerSearchResponse> { + let search_url = format!("{}/api/v1/search", self.server_url); + + #[derive(serde::Serialize)] + struct SearchRequest<'a> { + #[serde(rename = "q")] + q: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + limit: Option<usize>, + #[serde(skip_serializing_if = "Option::is_none")] + offset: Option<usize>, + #[serde(rename = "source_id", skip_serializing_if = "Option::is_none")] + source_id: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + source_tags: Option<Vec<SourceTag>>, + #[serde(skip_serializing_if = "Option::is_none")] + retrieve_data: Option<bool>, + } + + #[derive(serde::Deserialize)] + struct SearchResponse { + items: Vec<SearchItem>, + offset: usize, + total: usize, + } + + #[derive(serde::Deserialize)] + struct SearchItem { + id: String, + kind: String, + #[serde(default)] + name: Option<String>, + #[serde(default)] + source_id: Option<Uuid>, + #[serde(default)] + data: Option<Vec<u8>>, + } + + let source_id_str = query.source.map(|s| s.to_string()); + let request = SearchRequest { + q: &query.query, + limit: query.limit, + offset: query.offset, + source_id: source_id_str, + source_tags: match query.tags.is_empty() { + true => None, + false => Some(query.tags.clone()), + }, + // This must be passed to retrieve the function and type data. + retrieve_data: Some(true), + }; + + let resp = match self.client.get(search_url).query(&request).send() { + Ok(r) => r, + Err(err) => { + log::error!("Failed to send search request: {}", err); + return None; + } + }; + + let Ok(parsed) = resp.json::<SearchResponse>() else { + log::error!("Failed to parse search response"); + return None; + }; + + // TODO: This is quite scuffed, but it works for now. (Mostly just that it looks bad and queries a lot) + // TODO: Here I think would be a good place to sort it so sources always come first. + // TODO: Users searching will want to get to the source first, likely to whitelist or blacklist. + let mut items = Vec::with_capacity(parsed.items.len()); + for item in parsed.items { + let Some(source_uuid) = item.source_id else { + // Currently not interested in items without a source id. + // Things like symbols do not have a source id. + continue; + }; + + let kind = match item.kind.as_str() { + "function" => { + let Some(data) = &item.data else { + log::warn!( + "Function item {} has no data from network, skipping...", + item.id + ); + continue; + }; + let Some(func) = Function::from_bytes(&data) else { + log::warn!( + "Function item {} has invalid data from network, skipping...", + item.id + ); + continue; + }; + ContainerSearchItemKind::Function(func) + } + "source" => ContainerSearchItemKind::Source { + path: match item.name { + None => { + log::warn!("Source item {} has no name", item.id); + continue; + } + Some(name) => SourcePath(format!("{}/{}", self.server_url, name).into()), + }, + id: SourceId(source_uuid), + }, + "type" => { + let Some(data) = &item.data else { + log::warn!( + "Type item {} has no data from network, skipping...", + item.id + ); + continue; + }; + let Some(ty) = Type::from_bytes(&data) else { + log::warn!( + "Type item {} has invalid data from network, skipping...", + item.id + ); + continue; + }; + ContainerSearchItemKind::Type(ty) + } + _ => continue, + }; + + items.push(ContainerSearchItem { + source: SourceId(source_uuid), + kind, + }); + } + + Some(ContainerSearchResponse { + items, + total: parsed.total, + offset: parsed.offset, + }) + } } diff --git a/plugins/warp/src/convert/types.rs b/plugins/warp/src/convert/types.rs index 1dfe5fa9..2401a039 100644 --- a/plugins/warp/src/convert/types.rs +++ b/plugins/warp/src/convert/types.rs @@ -279,9 +279,10 @@ pub fn to_bn_calling_convention<A: BNArchitecture>( arch.get_default_calling_convention().unwrap() } -pub fn to_bn_type<A: BNArchitecture>(arch: &A, ty: &Type) -> BNRef<BNType> { +// Always pass the architecture unless you know what you're doing! +pub fn to_bn_type<A: BNArchitecture + Copy>(arch: Option<A>, ty: &Type) -> BNRef<BNType> { let bits_to_bytes = |val: u64| (val / 8); - let addr_size = arch.address_size() as u64; + let addr_size = arch.map(|a| a.address_size()).unwrap_or(8) as u64; match &ty.class { TypeClass::Void => BNType::void(), TypeClass::Boolean(_) => BNType::bool(), @@ -438,8 +439,8 @@ pub fn to_bn_type<A: BNArchitecture>(arch: &A, ty: &Type) -> BNRef<BNType> { // TODO: Variable arguments let variable_args = false; // If we have a calling convention we run the extended function type creation. - match c.calling_convention.as_ref() { - Some(cc) => { + match (c.calling_convention.as_ref(), arch.as_ref()) { + (Some(cc), Some(arch)) => { let calling_convention = to_bn_calling_convention(arch, cc); BNType::function_with_opts( &return_type, @@ -449,7 +450,7 @@ pub fn to_bn_type<A: BNArchitecture>(arch: &A, ty: &Type) -> BNRef<BNType> { BNConf::new(0, 0), ) } - None => BNType::function(&return_type, params, variable_args), + (_, _) => BNType::function(&return_type, params, variable_args), } } TypeClass::Referrer(c) => { diff --git a/plugins/warp/src/matcher.rs b/plugins/warp/src/matcher.rs index 52a5b04f..d97b4a18 100644 --- a/plugins/warp/src/matcher.rs +++ b/plugins/warp/src/matcher.rs @@ -100,21 +100,21 @@ impl Matcher { // TODO: I would really like for WARP types to be added in a seperate type container, so that we don't // TODO: just add them as system or user types. - pub fn add_type_to_view<A: BNArchitecture>( + pub fn add_type_to_view<A: BNArchitecture + Copy>( &self, container: &dyn Container, source: &SourceId, view: &BinaryView, - arch: &A, + arch: A, ty: &Type, ) where Self: Sized, { - fn inner_add_type_to_view<A: BNArchitecture>( + fn inner_add_type_to_view<A: BNArchitecture + Copy>( container: &dyn Container, source: &SourceId, view: &BinaryView, - arch: &A, + arch: A, visited_refs: &mut HashSet<String>, ty: &Type, ) { @@ -251,7 +251,7 @@ impl Matcher { view.define_auto_type_with_id( name, &guid.to_string(), - &to_bn_type(arch, &ref_ty), + &to_bn_type(Some(arch), &ref_ty), ); } (Some(_guid), Some(_name), None) => { diff --git a/plugins/warp/src/plugin.rs b/plugins/warp/src/plugin.rs index 7a2bdcff..229b0ae0 100644 --- a/plugins/warp/src/plugin.rs +++ b/plugins/warp/src/plugin.rs @@ -14,10 +14,11 @@ use binaryninja::command::{ }; use binaryninja::is_ui_enabled; use binaryninja::logger::Logger; -use binaryninja::settings::Settings; +use binaryninja::settings::{QueryOptions, Settings}; use log::LevelFilter; use reqwest::StatusCode; +mod commit; mod create; mod debug; mod ffi; @@ -31,17 +32,21 @@ mod workflow; fn load_bundled_signatures() { let global_bn_settings = Settings::new(); - let plugin_settings = PluginSettings::from_settings(&global_bn_settings); + let plugin_settings = + PluginSettings::from_settings(&global_bn_settings, &mut QueryOptions::new()); // We want to load all the bundled directories into the container cache. let background_task = BackgroundTask::new("Loading WARP files...", false); let start = Instant::now(); if plugin_settings.load_bundled_files { - let core_disk_container = DiskContainer::new_from_dir(core_signature_dir()); + let mut core_disk_container = DiskContainer::new_from_dir(core_signature_dir()); + core_disk_container.name = "Bundled".to_string(); + core_disk_container.writable = false; log::debug!("{:#?}", core_disk_container); add_cached_container(core_disk_container); } if plugin_settings.load_user_files { - let user_disk_container = DiskContainer::new_from_dir(user_signature_dir()); + let mut user_disk_container = DiskContainer::new_from_dir(user_signature_dir()); + user_disk_container.name = "User".to_string(); log::debug!("{:#?}", user_disk_container); add_cached_container(user_disk_container); } @@ -51,36 +56,77 @@ fn load_bundled_signatures() { fn load_network_container() { let global_bn_settings = Settings::new(); - let plugin_settings = PluginSettings::from_settings(&global_bn_settings); - let background_task = BackgroundTask::new("Initializing WARP server...", false); - let start = Instant::now(); - if plugin_settings.enable_server { - let server_url = plugin_settings.server_url.clone(); - let server_api_key = plugin_settings.server_api_key.clone(); + + let add_network_container = |url: String, api_key: Option<String>| { let https_proxy_str = global_bn_settings.get_string("network.httpsProxy"); let https_proxy = if https_proxy_str.is_empty() { None } else { Some(https_proxy_str) }; - match NetworkClient::new(server_url.clone(), server_api_key, https_proxy) { + match NetworkClient::new(url.clone(), api_key.clone(), https_proxy) { Ok(network_client) => { // Before constructing the container, let's make sure that the server is OK. if let Ok(StatusCode::OK) = network_client.status() { - let network_container = NetworkContainer::new(network_client); + // Check if the user is logged in. If so, we should collect the writable sources. + let mut writable_sources = Vec::new(); + match network_client.current_user() { + Ok((id, username)) => { + log::info!( + "Server '{}' connected, logged in as user '{}'", + url, + username + ); + match network_client.query_sources(Some(id)) { + Ok(sources) => { + writable_sources = sources; + } + Err(e) => { + log::error!( + "Server '{}' failed to get sources for user: {}", + url, + e + ); + } + } + } + Err(e) if api_key.is_some() => { + log::error!( + "Server '{}' failed to authenticate with provided API key: {}", + url, + e + ); + } + Err(_) => { + log::info!("Server '{}' connected, logged in as guest", url); + } + } + + // TODO: Make the cache path include the domain or url, so that we can have multiple servers. + let main_cache_path = NetworkContainer::root_cache_location().join("main"); + let network_container = + NetworkContainer::new(network_client, main_cache_path, &writable_sources); log::debug!("{:#?}", network_container); add_cached_container(network_container); } else { - log::error!( - "Server '{}' is not reachable, disabling container...", - server_url - ); + log::error!("Server '{}' is not reachable, disabling container...", url); } } Err(e) => { log::error!("Failed to add networked container: {}", e); } } + }; + + let plugin_settings = + PluginSettings::from_settings(&global_bn_settings, &mut QueryOptions::new()); + let background_task = BackgroundTask::new("Initializing WARP server...", false); + let start = Instant::now(); + if plugin_settings.enable_server { + add_network_container(plugin_settings.server_url, plugin_settings.server_api_key); + if let Some(second_server_url) = plugin_settings.second_server_url { + add_network_container(second_server_url, plugin_settings.second_server_api_key); + } } log::debug!("Initializing warp server took {:?}", start.elapsed()); background_task.finish(); @@ -156,6 +202,12 @@ pub extern "C" fn CorePluginInit() -> bool { load::LoadSignatureFile {}, ); + register_command( + "WARP\\Commit File", + "Commit file to a source", + commit::CommitFile {}, + ); + register_command_for_function( "WARP\\Include Function", "Add current function to the list of functions to add to the signature file", diff --git a/plugins/warp/src/plugin/commit.rs b/plugins/warp/src/plugin/commit.rs new file mode 100644 index 00000000..2af5fc3e --- /dev/null +++ b/plugins/warp/src/plugin/commit.rs @@ -0,0 +1,150 @@ +//! Commit file to a source. + +use crate::cache::container::cached_containers; +use crate::container::{SourceId, SourcePath}; +use crate::plugin::create::OpenFileField; +use binaryninja::binary_view::BinaryView; +use binaryninja::command::Command; +use binaryninja::interaction::{Form, FormInputField}; +use warp::chunk::ChunkKind; +use warp::WarpFile; + +pub struct SelectedSourceField { + sources: Vec<(SourceId, SourcePath)>, +} + +impl SelectedSourceField { + pub fn field(&self) -> FormInputField { + FormInputField::Choice { + prompt: "Selected Source".to_string(), + choices: self + .sources + .iter() + .map(|(id, path)| { + // For display purposes we only want to show the last path item. + let path_name = path + .to_string() + .rsplit_once('/') + .map_or(path.to_string(), |(_, last_path_item)| { + last_path_item.to_string() + }); + // TODO: Probably have a truncation limit here, this is just for display after all. + format!("{} ({})", path_name, id) + }) + .collect(), + default: None, + value: 0, + } + } + + pub fn from_form(&self, form: &Form) -> Option<SourceId> { + let field = form.get_field_with_name("Selected Source")?; + let field_value = field.try_value_index()?; + self.sources.get(field_value).map(|(id, _)| *id) + } +} + +pub struct CommitFile; + +impl CommitFile { + pub fn selected_source_field() -> SelectedSourceField { + let mut writable_sources = Vec::new(); + for container in cached_containers() { + if let Ok(container) = container.read() { + for source in container.sources().unwrap_or_default() { + if let Ok(true) = container.is_source_writable(&source) { + if let Ok(source_path) = container.source_path(&source) { + writable_sources.push((source, source_path)); + } + } + } + } + } + SelectedSourceField { + sources: writable_sources, + } + } + + pub fn execute() -> Option<()> { + let mut form = Form::new("Commit File"); + + // Users are going to get confused between this and adding functions to a source then commiting. + // So we should make it clear with a label, and also probably deprecate this command and replace it with "add functions to source" and "commit source". + form.add_field(FormInputField::Label { + prompt: "Commits a WARP file to an existing source, this is primarily used for committing to network containers".to_string() + }); + + form.add_field(OpenFileField::field()); + let source_field = Self::selected_source_field(); + form.add_field(source_field.field()); + + if !form.prompt() { + return None; + } + + let open_file_path = OpenFileField::from_form(&form)?; + let source_id = source_field.from_form(&form)?; + log::info!("Committing file to source: {}", source_id); + + let bytes = std::fs::read(open_file_path).ok()?; + let Some(warp_file) = WarpFile::from_bytes(&bytes) else { + log::error!("Failed to parse warp file!"); + return None; + }; + + for container in cached_containers() { + let Ok(mut container) = container.write() else { + continue; + }; + + if let Ok(true) = container.is_source_writable(&source_id) { + // TODO: We need to find a sane way to do this procedure through the FFI. + for chunk in &warp_file.chunks { + match &chunk.kind { + ChunkKind::Signature(sc) => { + let functions: Vec<_> = sc.functions().collect(); + log::info!( + "Adding {} functions to source: {}", + functions.len(), + source_id + ); + if let Err(e) = container.add_functions( + &chunk.header.target, + &source_id, + &functions, + ) { + log::error!("Failed to add functions to source: {}", e); + } + } + ChunkKind::Type(sc) => { + let types: Vec<_> = sc.types().collect(); + log::info!("Adding {} types to source: {}", types.len(), source_id); + if let Err(e) = container.add_computed_types(&source_id, &types) { + log::error!("Failed to add types to source: {}", e); + } + } + } + } + if let Err(e) = container.commit_source(&source_id) { + log::error!("Failed to commit source: {}", e); + } + log::info!("Committed file to source: {}", source_id); + return Some(()); + } + } + + Some(()) + } +} + +impl Command for CommitFile { + fn action(&self, _view: &BinaryView) { + std::thread::spawn(move || { + Self::execute(); + }); + } + + fn valid(&self, _view: &BinaryView) -> bool { + true + } +} diff --git a/plugins/warp/src/plugin/ffi.rs b/plugins/warp/src/plugin/ffi.rs index 2826ed34..c1f5acb8 100644 --- a/plugins/warp/src/plugin/ffi.rs +++ b/plugins/warp/src/plugin/ffi.rs @@ -1,4 +1,5 @@ mod container; +mod file; mod function; use binaryninjacore_sys::{ @@ -88,6 +89,17 @@ pub unsafe extern "C" fn BNWARPUUIDGetString(uuid: *const Uuid) -> *mut c_char { } #[no_mangle] +pub unsafe extern "C" fn BNWARPUUIDFromString(uuid_str: *mut c_char, uuid: *mut Uuid) -> bool { + if let Ok(uuid_str) = std::ffi::CStr::from_ptr(uuid_str).to_str() { + if let Some(parsed_uuid) = Uuid::parse_str(uuid_str).ok() { + *uuid = parsed_uuid; + return true; + } + } + false +} + +#[no_mangle] pub unsafe extern "C" fn BNWARPUUIDEqual(a: *const Uuid, b: *const Uuid) -> bool { (*a) == (*b) } diff --git a/plugins/warp/src/plugin/ffi/container.rs b/plugins/warp/src/plugin/ffi/container.rs index 4de6bab3..72c85366 100644 --- a/plugins/warp/src/plugin/ffi/container.rs +++ b/plugins/warp/src/plugin/ffi/container.rs @@ -1,5 +1,7 @@ use crate::cache::container::cached_containers; -use crate::container::SourcePath; +use crate::container::{ + ContainerSearchItem, ContainerSearchItemKind, ContainerSearchQuery, SourcePath, SourceTag, +}; use crate::convert::{from_bn_type, to_bn_type}; use crate::plugin::ffi::{ BNWARPContainer, BNWARPFunction, BNWARPFunctionGUID, BNWARPSource, BNWARPTarget, BNWARPTypeGUID, @@ -12,7 +14,163 @@ use binaryninja::types::Type; use binaryninjacore_sys::{BNArchitecture, BNBinaryView, BNType}; use std::ffi::{c_char, CStr}; use std::mem::ManuallyDrop; +use std::ops::Deref; use std::sync::Arc; +use warp::r#type::guid::TypeGUID; + +pub type BNWARPContainerSearchQuery = ContainerSearchQuery; +pub type BNWARPContainerSearchItem = ContainerSearchItem; + +#[repr(C)] +pub enum BNWARPContainerSearchItemKind { + Source = 0, + Function = 1, + Type = 2, + Symbol = 3, +} + +#[repr(C)] +pub struct BNWARPContainerSearchResponse { + pub count: usize, + pub items: *mut *mut BNWARPContainerSearchItem, + pub offset: usize, + pub total: usize, +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPNewContainerSearchQuery( + query: *mut c_char, + offset: *const usize, + limit: *const usize, + source: *const BNWARPSource, + source_tags: *mut *mut c_char, + source_tags_count: usize, +) -> *mut BNWARPContainerSearchQuery { + let query_cstr = unsafe { CStr::from_ptr(query) }; + let Ok(query) = query_cstr.to_str() else { + return std::ptr::null_mut(); + }; + let mut search_query = ContainerSearchQuery::new(query.to_string()); + if !offset.is_null() { + search_query.offset = Some(*offset); + } + if !limit.is_null() { + search_query.limit = Some(*limit); + } + if !source.is_null() { + search_query.source = Some(*source); + } + if !source_tags.is_null() { + let source_tags_raw = unsafe { std::slice::from_raw_parts(source_tags, source_tags_count) }; + let source_tags: Vec<SourceTag> = source_tags_raw + .iter() + .filter_map(|&ptr| CStr::from_ptr(ptr).to_str().ok()) + .map(|s| s.into()) + .collect(); + search_query.tags = source_tags; + } + Box::into_raw(Box::new(search_query)) +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerSearchItemGetKind( + item: *mut BNWARPContainerSearchItem, +) -> BNWARPContainerSearchItemKind { + let item = ManuallyDrop::new(Arc::from_raw(item)); + match &item.kind { + ContainerSearchItemKind::Source { .. } => BNWARPContainerSearchItemKind::Source, + ContainerSearchItemKind::Function(_) => BNWARPContainerSearchItemKind::Function, + ContainerSearchItemKind::Type(_) => BNWARPContainerSearchItemKind::Type, + ContainerSearchItemKind::Symbol(_) => BNWARPContainerSearchItemKind::Symbol, + } +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerSearchItemGetSource( + item: *mut BNWARPContainerSearchItem, +) -> BNWARPSource { + let item = ManuallyDrop::new(Arc::from_raw(item)); + item.source +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerSearchItemGetType( + arch: *mut BNArchitecture, + item: *mut BNWARPContainerSearchItem, +) -> *mut BNType { + // NOTE: to convert the type, we must have an architecture. + let arch = match !arch.is_null() { + true => Some(CoreArchitecture::from_raw(arch)), + false => None, + }; + + let item = ManuallyDrop::new(Arc::from_raw(item)); + match &item.kind { + ContainerSearchItemKind::Source { .. } => std::ptr::null_mut(), + ContainerSearchItemKind::Function(func) => { + match &func.ty { + None => std::ptr::null_mut(), + Some(ty) => { + let bn_ty = to_bn_type(arch, &ty); + // NOTE: The type ref has been pre-incremented for the caller. + unsafe { Ref::into_raw(bn_ty) }.handle + } + } + } + ContainerSearchItemKind::Type(ty) => { + let bn_ty = to_bn_type(arch, &ty); + // NOTE: The type ref has been pre-incremented for the caller. + unsafe { Ref::into_raw(bn_ty) }.handle + } + ContainerSearchItemKind::Symbol(_) => std::ptr::null_mut(), + } +} + +// NOTE: In the future we should allow for the possibility of this returning a null pointer. +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerSearchItemGetName( + item: *mut BNWARPContainerSearchItem, +) -> *mut c_char { + let item = ManuallyDrop::new(Arc::from_raw(item)); + match &item.kind { + ContainerSearchItemKind::Source { path, .. } => { + let bn_name = BnString::new(path.to_string()); + BnString::into_raw(bn_name) + } + ContainerSearchItemKind::Function(func) => { + let bn_name = BnString::new(func.symbol.name.clone()); + BnString::into_raw(bn_name) + } + ContainerSearchItemKind::Type(ty) => { + // TODO: Maybe un-named types should return std::ptr::null_mut()? + let ty_name = ty + .name + .clone() + .unwrap_or_else(|| TypeGUID::from(ty).to_string()); + let bn_name = BnString::new(ty_name); + BnString::into_raw(bn_name) + } + ContainerSearchItemKind::Symbol(sym) => { + let bn_name = BnString::new(sym.name.clone()); + BnString::into_raw(bn_name) + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPContainerSearchItemGetFunction( + item: *mut BNWARPContainerSearchItem, +) -> *mut BNWARPFunction { + let item = ManuallyDrop::new(Arc::from_raw(item)); + match &item.kind { + ContainerSearchItemKind::Source { .. } => std::ptr::null_mut(), + ContainerSearchItemKind::Function(func) => { + Arc::into_raw(Arc::new(func.clone())) as *mut BNWARPFunction + } + ContainerSearchItemKind::Type(_) => std::ptr::null_mut(), + ContainerSearchItemKind::Symbol(_) => std::ptr::null_mut(), + } +} #[no_mangle] pub unsafe extern "C" fn BNWARPGetContainers(count: *mut usize) -> *mut *mut BNWARPContainer { @@ -39,6 +197,8 @@ pub unsafe extern "C" fn BNWARPContainerGetName(container: *mut BNWARPContainer) pub unsafe extern "C" fn BNWARPContainerFetchFunctions( container: *mut BNWARPContainer, target: *mut BNWARPTarget, + source_tags: *mut *mut c_char, + source_tags_count: usize, guids: *const BNWARPFunctionGUID, count: usize, ) { @@ -49,9 +209,16 @@ pub unsafe extern "C" fn BNWARPContainerFetchFunctions( let target = unsafe { ManuallyDrop::new(Arc::from_raw(target)) }; + let source_tags_raw = unsafe { std::slice::from_raw_parts(source_tags, source_tags_count) }; + let source_tags: Vec<SourceTag> = source_tags_raw + .iter() + .filter_map(|&ptr| CStr::from_ptr(ptr).to_str().ok()) + .map(|s| s.into()) + .collect(); + let guids = unsafe { std::slice::from_raw_parts(guids, count) }; - if let Err(e) = container.fetch_functions(&target, guids) { + if let Err(e) = container.fetch_functions(&target, &source_tags, guids) { log::error!("Failed to fetch functions: {}", e); } } @@ -373,7 +540,7 @@ pub unsafe extern "C" fn BNWARPContainerGetTypeWithGUID( let Some(ty) = container.type_with_guid(&source, &guid).unwrap_or_default() else { return std::ptr::null_mut(); }; - let function_type = to_bn_type(&arch, &ty); + let function_type = to_bn_type(Some(arch), &ty); // NOTE: The type ref has been pre-incremented for the caller. unsafe { Ref::into_raw(function_type) }.handle } @@ -405,6 +572,45 @@ pub unsafe extern "C" fn BNWARPContainerGetTypeGUIDsWithName( } #[no_mangle] +pub unsafe extern "C" fn BNWARPContainerSearch( + container: *mut BNWARPContainer, + query: *mut BNWARPContainerSearchQuery, +) -> *mut BNWARPContainerSearchResponse { + let arc_container = ManuallyDrop::new(Arc::from_raw(container)); + let Ok(container) = arc_container.read() else { + return std::ptr::null_mut(); + }; + + let query = unsafe { ManuallyDrop::new(Arc::from_raw(query)) }; + + let result = match container.search(&query) { + Ok(result) => result, + Err(err) => { + log::error!("Failed to search container {:?}: {}", query.deref(), err); + return std::ptr::null_mut(); + } + }; + + let boxed_raw_items: Box<[_]> = result + .items + .into_iter() + .map(Arc::new) + .map(Arc::into_raw) + .collect(); + let count = boxed_raw_items.len(); + // NOTE: Leak the functions to be freed by BNWARPFreeContainerSearchItemList + let leaked_raw_items = Box::into_raw(boxed_raw_items) as *mut *mut BNWARPContainerSearchItem; + let raw_result = BNWARPContainerSearchResponse { + count, + items: leaked_raw_items, + total: result.total, + offset: result.offset, + }; + // NOTE: Leak the result to be freed by BNWARPFreeContainerSearchResult + Box::into_raw(Box::new(raw_result)) +} + +#[no_mangle] pub unsafe extern "C" fn BNWARPNewContainerReference( container: *mut BNWARPContainer, ) -> *mut BNWARPContainer { @@ -431,3 +637,62 @@ pub unsafe extern "C" fn BNWARPFreeContainerList( BNWARPFreeContainerReference(container); } } + +#[no_mangle] +pub unsafe extern "C" fn BNWARPNewContainerSearchQueryReference( + query: *mut BNWARPContainerSearchQuery, +) -> *mut BNWARPContainerSearchQuery { + Arc::increment_strong_count(query); + query +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFreeContainerSearchQueryReference( + query: *mut BNWARPContainerSearchQuery, +) { + if query.is_null() { + return; + } + Arc::decrement_strong_count(query); +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPNewContainerSearchItemReference( + item: *mut BNWARPContainerSearchItem, +) -> *mut BNWARPContainerSearchItem { + Arc::increment_strong_count(item); + item +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFreeContainerSearchItemReference( + item: *mut BNWARPContainerSearchItem, +) { + if item.is_null() { + return; + } + Arc::decrement_strong_count(item); +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFreeContainerSearchItemList( + items: *mut *mut BNWARPContainerSearchItem, + count: usize, +) { + let items_ptr = std::ptr::slice_from_raw_parts_mut(items, count); + let items = unsafe { Box::from_raw(items_ptr) }; + for item in items { + BNWARPFreeContainerSearchItemReference(item); + } +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFreeContainerSearchResponse( + response: *mut BNWARPContainerSearchResponse, +) { + if response.is_null() { + return; + } + let response = unsafe { Box::from_raw(response) }; + BNWARPFreeContainerSearchItemList(response.items, response.count); +} diff --git a/plugins/warp/src/plugin/ffi/file.rs b/plugins/warp/src/plugin/ffi/file.rs new file mode 100644 index 00000000..951b5eb2 --- /dev/null +++ b/plugins/warp/src/plugin/ffi/file.rs @@ -0,0 +1,38 @@ +use std::ffi::c_char; +use std::sync::Arc; +use warp::WarpFile; + +pub type BNWARPFile = WarpFile<'static>; + +// TODO: At some point we may want to expose chunks directly. For now we will just enumerate all of them. +// pub type BNWARPChunk = warp::chunk::Chunk<'static>; + +// TODO: From bytes as well. +#[no_mangle] +pub unsafe extern "C" fn BNWARPNewFileFromPath(path: *mut c_char) -> *mut BNWARPFile { + let path_cstr = unsafe { std::ffi::CStr::from_ptr(path) }; + let Ok(path) = path_cstr.to_str() else { + return std::ptr::null_mut(); + }; + let Ok(bytes) = std::fs::read(path) else { + return std::ptr::null_mut(); + }; + let Some(file) = WarpFile::from_owned_bytes(bytes) else { + return std::ptr::null_mut(); + }; + Arc::into_raw(Arc::new(file)) as *mut BNWARPFile +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPNewFileReference(file: *mut BNWARPFile) -> *mut BNWARPFile { + Arc::increment_strong_count(file); + file +} + +#[no_mangle] +pub unsafe extern "C" fn BNWARPFreeFileReference(file: *mut BNWARPFile) { + if file.is_null() { + return; + } + Arc::decrement_strong_count(file); +} diff --git a/plugins/warp/src/plugin/ffi/function.rs b/plugins/warp/src/plugin/ffi/function.rs index b37d613b..e3f6a079 100644 --- a/plugins/warp/src/plugin/ffi/function.rs +++ b/plugins/warp/src/plugin/ffi/function.rs @@ -124,7 +124,7 @@ pub unsafe extern "C" fn BNWARPFunctionGetType( match &function.ty { Some(func_ty) => { let arch = analysis_function.arch(); - let function_type = to_bn_type(&arch, func_ty); + let function_type = to_bn_type(Some(arch), func_ty); // NOTE: The type ref has been pre-incremented for the caller. unsafe { Ref::into_raw(function_type) }.handle } diff --git a/plugins/warp/src/plugin/load.rs b/plugins/warp/src/plugin/load.rs index cef6ea11..6f8b562e 100644 --- a/plugins/warp/src/plugin/load.rs +++ b/plugins/warp/src/plugin/load.rs @@ -40,16 +40,15 @@ pub struct RunMatcherField; impl RunMatcherField { pub fn field() -> FormInputField { - FormInputField::Choice { - prompt: "Rerun Initial Matcher".to_string(), - choices: vec!["No".to_string(), "Yes".to_string()], - default: Some(1), - value: 0, + FormInputField::Checkbox { + prompt: "Rerun Matcher".to_string(), + default: Some(true), + value: false, } } pub fn from_form(form: &Form) -> Option<bool> { - let field = form.get_field_with_name("Rerun Initial Matcher")?; + let field = form.get_field_with_name("Rerun Matcher")?; let field_value = field.try_value_index()?; match field_value { 1 => Some(true), diff --git a/plugins/warp/src/plugin/settings.rs b/plugins/warp/src/plugin/settings.rs index 489deb23..39092177 100644 --- a/plugins/warp/src/plugin/settings.rs +++ b/plugins/warp/src/plugin/settings.rs @@ -1,4 +1,4 @@ -use binaryninja::settings::Settings as BNSettings; +use binaryninja::settings::{QueryOptions, Settings as BNSettings}; use serde_json::json; use std::string::ToString; @@ -20,6 +20,12 @@ pub struct PluginSettings { /// /// This is set to [PluginSettings::SERVER_API_KEY_DEFAULT] by default. pub server_api_key: Option<String>, + pub second_server_url: Option<String>, + pub second_server_api_key: Option<String>, + /// A source must have at least one of these tags to be considered a valid source. + /// + /// This is set to [PluginSettings::SOURCE_TAGS_DEFAULT] by default. + pub whitelisted_source_tags: Vec<String>, /// Whether to allow networked WARP requests. Turning this off will not disable local WARP functionality. /// /// This is set to [PluginSettings::ENABLE_SERVER_DEFAULT] by default. @@ -27,6 +33,8 @@ pub struct PluginSettings { } impl PluginSettings { + pub const WHITELISTED_SOURCE_TAGS_DEFAULT: Vec<String> = vec![]; + pub const WHITELISTED_SOURCE_TAGS_SETTING: &'static str = "analysis.warp.whitelistedSourceTags"; pub const LOAD_BUNDLED_FILES_DEFAULT: bool = true; pub const LOAD_BUNDLED_FILES_SETTING: &'static str = "analysis.warp.loadBundledFiles"; pub const LOAD_USER_FILES_DEFAULT: bool = true; @@ -35,10 +43,25 @@ impl PluginSettings { pub const SERVER_URL_SETTING: &'static str = "analysis.warp.serverUrl"; pub const SERVER_API_KEY_DEFAULT: Option<String> = None; pub const SERVER_API_KEY_SETTING: &'static str = "analysis.warp.serverApiKey"; + pub const SECONDARY_SERVER_URL_DEFAULT: Option<String> = None; + pub const SECONDARY_SERVER_URL_SETTING: &'static str = "analysis.warp.secondServerUrl"; + pub const SECONDARY_SERVER_API_KEY_DEFAULT: Option<String> = None; + pub const SECONDARY_SERVER_API_KEY_SETTING: &'static str = "analysis.warp.secondServerApiKey"; pub const ENABLE_SERVER_DEFAULT: bool = false; pub const ENABLE_SERVER_SETTING: &'static str = "network.enableWARP"; pub fn register(bn_settings: &mut BNSettings) { + let whitelisted_source_tags_prop = json!({ + "title" : "Blacklisted Sources", + "type" : "array", + "default" : Self::WHITELISTED_SOURCE_TAGS_DEFAULT, + "description" : "Add a sources UUID to this list to blacklist it from being considered a valid source. This is useful for sources that are known to be false positives.", + "ignore" : [], + }); + bn_settings.register_setting_json( + Self::WHITELISTED_SOURCE_TAGS_SETTING, + &whitelisted_source_tags_prop.to_string(), + ); let load_bundled_files_prop = json!({ "title" : "Load Bundled Files", "type" : "boolean", @@ -85,6 +108,31 @@ impl PluginSettings { Self::SERVER_API_KEY_SETTING, &server_api_key_prop.to_string(), ); + let second_server_url_prop = json!({ + "title" : "Secondary Server URL", + "type" : "string", + "default" : Self::SECONDARY_SERVER_URL_DEFAULT, + "description" : "", + "ignore" : ["SettingsProjectScope", "SettingsResourceScope"], + "requiresRestart" : true + }); + bn_settings.register_setting_json( + Self::SECONDARY_SERVER_URL_SETTING, + &second_server_url_prop.to_string(), + ); + let second_server_api_key_prop = json!({ + "title" : "Secondary Server API Key", + "type" : "string", + "default" : Self::SECONDARY_SERVER_API_KEY_DEFAULT, + "description" : "", + "ignore" : ["SettingsProjectScope", "SettingsResourceScope"], + "hidden": true, + "requiresRestart" : true + }); + bn_settings.register_setting_json( + Self::SECONDARY_SERVER_API_KEY_SETTING, + &second_server_api_key_prop.to_string(), + ); let server_enabled_prop = json!({ "title" : "Enable WARP", "type" : "boolean", @@ -100,7 +148,7 @@ impl PluginSettings { } /// Retrieve plugin settings from [`BNSettings`]. - pub fn from_settings(bn_settings: &BNSettings) -> Self { + pub fn from_settings(bn_settings: &BNSettings, query_opts: &mut QueryOptions) -> Self { let mut settings = PluginSettings::default(); if bn_settings.contains(Self::LOAD_BUNDLED_FILES_SETTING) { settings.load_bundled_files = bn_settings.get_bool(Self::LOAD_BUNDLED_FILES_SETTING); @@ -117,9 +165,30 @@ impl PluginSettings { settings.server_api_key = Some(server_api_key_str); } } + if bn_settings.contains(Self::SECONDARY_SERVER_URL_SETTING) { + let server_api_key_str = bn_settings.get_string(Self::SECONDARY_SERVER_URL_SETTING); + if !server_api_key_str.is_empty() { + settings.second_server_url = Some(server_api_key_str); + } + } + if bn_settings.contains(Self::SECONDARY_SERVER_API_KEY_SETTING) { + let server_api_key_str = bn_settings.get_string(Self::SECONDARY_SERVER_API_KEY_SETTING); + if !server_api_key_str.is_empty() { + settings.second_server_api_key = Some(server_api_key_str); + } + } if bn_settings.contains(Self::ENABLE_SERVER_SETTING) { settings.enable_server = bn_settings.get_bool(Self::ENABLE_SERVER_SETTING); } + + if bn_settings.contains(Self::WHITELISTED_SOURCE_TAGS_SETTING) { + let whitelisted_source_tags_str = bn_settings + .get_string_list_with_opts(Self::WHITELISTED_SOURCE_TAGS_SETTING, query_opts); + settings.whitelisted_source_tags = whitelisted_source_tags_str + .iter() + .map(|s| s.to_string()) + .collect(); + } settings } } @@ -127,10 +196,13 @@ impl PluginSettings { impl Default for PluginSettings { fn default() -> Self { Self { + whitelisted_source_tags: PluginSettings::WHITELISTED_SOURCE_TAGS_DEFAULT, load_bundled_files: PluginSettings::LOAD_BUNDLED_FILES_DEFAULT, load_user_files: PluginSettings::LOAD_USER_FILES_DEFAULT, server_url: PluginSettings::SERVER_URL_DEFAULT.to_string(), server_api_key: PluginSettings::SERVER_API_KEY_DEFAULT, + second_server_url: PluginSettings::SECONDARY_SERVER_URL_DEFAULT, + second_server_api_key: PluginSettings::SECONDARY_SERVER_API_KEY_DEFAULT, enable_server: PluginSettings::ENABLE_SERVER_DEFAULT, } } diff --git a/plugins/warp/src/plugin/workflow.rs b/plugins/warp/src/plugin/workflow.rs index 9871f5d2..08d07c52 100644 --- a/plugins/warp/src/plugin/workflow.rs +++ b/plugins/warp/src/plugin/workflow.rs @@ -162,11 +162,7 @@ pub fn run_matcher(view: &BinaryView) { log::info!("Matcher was cancelled by user, you may run it again by running the 'Run Matcher' command."); } - // It is noisy to show this every time, so we only show it in cases where a user can reasonably perceive. - let elapsed = start.elapsed(); - if elapsed > std::time::Duration::from_secs(1) { - log::info!("Function matching took {:?}", elapsed); - } + log::info!("Function matching took {:?}", start.elapsed()); background_task.finish(); // Now we want to trigger re-analysis. @@ -185,7 +181,7 @@ pub fn insert_workflow() -> Result<(), ()> { // otherwise we will wipe over user type info. if !function.has_user_type() { if let Some(func_ty) = &matched_function.ty { - function.set_auto_type(&to_bn_type(&function.arch(), func_ty)); + function.set_auto_type(&to_bn_type(Some(function.arch()), func_ty)); } } if let Some(mlil) = ctx.mlil_function() { @@ -206,7 +202,7 @@ pub fn insert_workflow() -> Result<(), ()> { continue; } let decl_ty = match variable.ty { - Some(decl_ty) => to_bn_type(&function.arch(), &decl_ty), + Some(decl_ty) => to_bn_type(Some(function.arch()), &decl_ty), None => { let Some(existing_var) = function.variable_type(&decl_var) else { continue; |
