Skip to main content

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}