malbox_plugin_sdk/plugin/guest.rs
1//! The [`GuestPlugin`] trait for plugins that run inside an analysis VM.
2//!
3//! Guest plugins follow a linear lifecycle per task: `on_start` sets up
4//! monitoring, `execute_sample` launches the sample, the SDK waits for
5//! the analysis timeout, then `on_stop` flushes results and tears down.
6
7use super::Plugin;
8use crate::context::Context;
9use crate::error::Result;
10use std::path::Path;
11
12/// Launch a sample using the platform-native process creation API.
13///
14/// This is the default implementation of [`GuestPlugin::execute_sample`].
15pub fn default_launch(sample_path: &Path) -> LaunchResult {
16 use tracing::{error, info};
17
18 if !sample_path.exists() {
19 error!(path = %sample_path.display(), "default_launch: sample file does not exist");
20 return LaunchResult::UseDefault;
21 }
22
23 info!(path = %sample_path.display(), "default_launch: launching sample");
24
25 #[cfg(target_os = "windows")]
26 let result = {
27 use std::os::windows::process::CommandExt;
28 const CREATE_NEW_CONSOLE: u32 = 0x00000010;
29 std::process::Command::new(sample_path)
30 .creation_flags(CREATE_NEW_CONSOLE)
31 .spawn()
32 };
33
34 #[cfg(not(target_os = "windows"))]
35 let result = std::process::Command::new(sample_path).spawn();
36
37 match result {
38 Ok(mut child) => {
39 let pid = child.id();
40 info!(
41 path = %sample_path.display(),
42 pid,
43 "default_launch: sample launched successfully"
44 );
45
46 std::thread::spawn(move || match child.wait() {
47 Ok(status) => {
48 tracing::info!(
49 pid,
50 exit_code = status.code(),
51 success = status.success(),
52 "default_launch: sample process exited"
53 );
54 }
55 Err(e) => {
56 tracing::warn!(
57 pid,
58 error = %e,
59 "default_launch: failed to wait on sample process"
60 );
61 }
62 });
63
64 LaunchResult::Launched
65 }
66 Err(e) => {
67 error!(
68 path = %sample_path.display(),
69 error = %e,
70 kind = ?e.kind(),
71 "default_launch: failed to launch sample"
72 );
73 LaunchResult::UseDefault
74 }
75 }
76}
77
78/// Outcome of [`GuestPlugin::execute_sample`].
79///
80/// Tells the SDK whether the plugin handled sample launch itself or
81/// wants the SDK to use the platform default.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum LaunchResult {
84 /// The plugin did not launch the sample. The SDK should use [`default_launch`].
85 UseDefault,
86 /// The plugin successfully launched the sample itself.
87 Launched,
88}
89
90/// Trait for malbox guest plugins that run inside an ephemeral VM.
91///
92/// The SDK owns the lifecycle sequence:
93/// 1. `on_start` - plugin sets up monitoring, receives context
94/// 2. `execute_sample` - SDK calls this to launch the sample (default: platform launcher)
95/// 3. SDK waits for analysis timeout
96/// 4. `on_stop` - plugin flushes results and tears down
97pub trait GuestPlugin: Plugin {
98 /// Called when the analysis task begins. Set up monitoring infrastructure
99 /// (ETW sessions, decoders, sinks, etc.) and return when ready to capture.
100 ///
101 /// The SDK guarantees `execute_sample` will not be called until this returns.
102 fn on_start(&self, ctx: &Context) -> Result<()>;
103
104 /// Called when the analysis timeout expires or the daemon signals shutdown.
105 /// Flush all buffered results via [`Context::results().push()`] and tear down.
106 fn on_stop(&self, ctx: &Context) -> Result<()>;
107
108 /// Launch the sample at the given path. Called by the SDK after `on_start`.
109 ///
110 /// The default implementation uses the platform's process creation API.
111 /// Override for non-EXE scenarios (DLL loading, COM dispatch, etc.).
112 fn execute_sample(&self, sample_path: &Path) -> Result<LaunchResult> {
113 Ok(default_launch(sample_path))
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 struct MinimalGuestPlugin;
122 impl Plugin for MinimalGuestPlugin {}
123 impl GuestPlugin for MinimalGuestPlugin {
124 fn on_start(&self, _ctx: &Context) -> Result<()> {
125 Ok(())
126 }
127 fn on_stop(&self, _ctx: &Context) -> Result<()> {
128 Ok(())
129 }
130 }
131
132 #[test]
133 fn minimal_guest_plugin_defaults_work() {
134 let plugin = MinimalGuestPlugin;
135 let health = plugin.health_check();
136 assert!(health.is_ready());
137 }
138
139 #[test]
140 fn execute_sample_has_default_impl() {
141 let plugin = MinimalGuestPlugin;
142 let _ = &plugin as &dyn GuestPlugin;
143 }
144}