diff --git a/implants/imix/src/tests/mod.rs b/implants/imix/src/tests/mod.rs index 360d833ab..03186a792 100644 --- a/implants/imix/src/tests/mod.rs +++ b/implants/imix/src/tests/mod.rs @@ -4,3 +4,5 @@ mod agent_trait_tests; mod callback_interval_test; mod report_large_file_test; mod task_tests; +pub mod syntax_error_test; +pub mod syntax_error_parse_test; diff --git a/implants/imix/src/tests/syntax_error_parse_test.rs b/implants/imix/src/tests/syntax_error_parse_test.rs new file mode 100644 index 000000000..76d876873 --- /dev/null +++ b/implants/imix/src/tests/syntax_error_parse_test.rs @@ -0,0 +1,157 @@ +use super::super::task::TaskRegistry; +use alloc::collections::{BTreeMap, BTreeSet}; +use eldritch::agent::agent::Agent; +use eldritch_agent::Context; +use pb::c2; +use pb::c2::report_output_request; +use pb::eldritch::Tome; +use std::sync::Arc; +use std::sync::Mutex; +use std::time::Duration; + +// Mock Agent specifically for TaskRegistry +struct MockAgent { + output_reports: Arc>>, +} + +impl MockAgent { + fn new() -> Self { + Self { + output_reports: Arc::new(Mutex::new(Vec::new())), + } + } +} + +impl Agent for MockAgent { + fn fetch_asset(&self, _req: c2::FetchAssetRequest) -> Result, String> { + Ok(vec![]) + } + fn report_credential( + &self, + _req: c2::ReportCredentialRequest, + ) -> Result { + Ok(c2::ReportCredentialResponse {}) + } + fn report_file( + &self, + _req: std::sync::mpsc::Receiver, + ) -> Result { + Ok(c2::ReportFileResponse {}) + } + fn report_process_list( + &self, + _req: c2::ReportProcessListRequest, + ) -> Result { + Ok(c2::ReportProcessListResponse {}) + } + fn report_output( + &self, + req: c2::ReportOutputRequest, + ) -> Result { + self.output_reports.lock().unwrap().push(req); + Ok(c2::ReportOutputResponse {}) + } + fn create_portal(&self, _context: Context) -> Result<(), String> { + Ok(()) + } + fn start_reverse_shell(&self, _context: Context, _cmd: Option) -> Result<(), String> { + Ok(()) + } + fn start_repl_reverse_shell(&self, _context: Context) -> Result<(), String> { + Ok(()) + } + fn claim_tasks(&self, _req: c2::ClaimTasksRequest) -> Result { + Ok(c2::ClaimTasksResponse { + tasks: vec![], + shell_tasks: vec![], + }) + } + fn get_config(&self) -> Result, String> { + Ok(BTreeMap::new()) + } + fn get_transport(&self) -> Result { + Ok("mock".to_string()) + } + fn set_transport(&self, _transport: String) -> Result<(), String> { + Ok(()) + } + fn list_transports(&self) -> Result, String> { + Ok(vec!["mock".to_string()]) + } + fn get_callback_interval(&self) -> Result { + Ok(5) + } + fn set_callback_interval(&self, _interval: u64) -> Result<(), String> { + Ok(()) + } + fn list_tasks(&self) -> Result, String> { + Ok(vec![]) + } + fn stop_task(&self, _task_id: i64) -> Result<(), String> { + Ok(()) + } + fn set_callback_uri(&self, _uri: String) -> std::result::Result<(), String> { + Ok(()) + } + fn list_callback_uris(&self) -> std::result::Result, String> { + Ok(BTreeSet::new()) + } + fn get_active_callback_uri(&self) -> std::result::Result { + Ok(String::new()) + } + fn get_next_callback_uri(&self) -> std::result::Result { + Ok(String::new()) + } + fn add_callback_uri(&self, _uri: String) -> std::result::Result<(), String> { + Ok(()) + } + fn remove_callback_uri(&self, _uri: String) -> std::result::Result<(), String> { + Ok(()) + } +} + +#[tokio::test] +async fn test_task_syntax_error_parse() { + let agent = Arc::new(MockAgent::new()); + let task_id = 405; + let code = "def my_func() {\n return 1\n}"; // syntax error in eldritch (python-like) + + let task = c2::Task { + id: task_id, + tome: Some(Tome { + eldritch: code.to_string(), + ..Default::default() + }), + quest_name: "syntax_error_parse_test".to_string(), + ..Default::default() + }; + + let registry = TaskRegistry::new(); + registry.spawn(task, agent.clone()); + + tokio::time::sleep(Duration::from_secs(3)).await; + + let reports = agent.output_reports.lock().unwrap(); + + // Check for error report + let error_report = reports.iter().find(|r| { + if let Some(report_output_request::Message::TaskOutput(m)) = &r.message { + if let Some(o) = &m.output { + return o.error.is_some(); + } + } + false + }); + + assert!(error_report.is_some(), "Should report error"); + if let Some(r) = error_report { + if let Some(report_output_request::Message::TaskOutput(m)) = &r.message { + if let Some(o) = &m.output { + if let Some(e) = &o.error { + println!("Error message: {}", e.msg); + assert!(e.msg.contains("Error") || e.msg.contains("Lexer") || e.msg.contains("Syntax")); + } + } + } + } +} diff --git a/implants/imix/src/tests/syntax_error_test.rs b/implants/imix/src/tests/syntax_error_test.rs new file mode 100644 index 000000000..4ef7d6f93 --- /dev/null +++ b/implants/imix/src/tests/syntax_error_test.rs @@ -0,0 +1,157 @@ +use super::super::task::TaskRegistry; +use alloc::collections::{BTreeMap, BTreeSet}; +use eldritch::agent::agent::Agent; +use eldritch_agent::Context; +use pb::c2; +use pb::c2::report_output_request; +use pb::eldritch::Tome; +use std::sync::Arc; +use std::sync::Mutex; +use std::time::Duration; + +// Mock Agent specifically for TaskRegistry +struct MockAgent { + output_reports: Arc>>, +} + +impl MockAgent { + fn new() -> Self { + Self { + output_reports: Arc::new(Mutex::new(Vec::new())), + } + } +} + +impl Agent for MockAgent { + fn fetch_asset(&self, _req: c2::FetchAssetRequest) -> Result, String> { + Ok(vec![]) + } + fn report_credential( + &self, + _req: c2::ReportCredentialRequest, + ) -> Result { + Ok(c2::ReportCredentialResponse {}) + } + fn report_file( + &self, + _req: std::sync::mpsc::Receiver, + ) -> Result { + Ok(c2::ReportFileResponse {}) + } + fn report_process_list( + &self, + _req: c2::ReportProcessListRequest, + ) -> Result { + Ok(c2::ReportProcessListResponse {}) + } + fn report_output( + &self, + req: c2::ReportOutputRequest, + ) -> Result { + self.output_reports.lock().unwrap().push(req); + Ok(c2::ReportOutputResponse {}) + } + fn create_portal(&self, _context: Context) -> Result<(), String> { + Ok(()) + } + fn start_reverse_shell(&self, _context: Context, _cmd: Option) -> Result<(), String> { + Ok(()) + } + fn start_repl_reverse_shell(&self, _context: Context) -> Result<(), String> { + Ok(()) + } + fn claim_tasks(&self, _req: c2::ClaimTasksRequest) -> Result { + Ok(c2::ClaimTasksResponse { + tasks: vec![], + shell_tasks: vec![], + }) + } + fn get_config(&self) -> Result, String> { + Ok(BTreeMap::new()) + } + fn get_transport(&self) -> Result { + Ok("mock".to_string()) + } + fn set_transport(&self, _transport: String) -> Result<(), String> { + Ok(()) + } + fn list_transports(&self) -> Result, String> { + Ok(vec!["mock".to_string()]) + } + fn get_callback_interval(&self) -> Result { + Ok(5) + } + fn set_callback_interval(&self, _interval: u64) -> Result<(), String> { + Ok(()) + } + fn list_tasks(&self) -> Result, String> { + Ok(vec![]) + } + fn stop_task(&self, _task_id: i64) -> Result<(), String> { + Ok(()) + } + fn set_callback_uri(&self, _uri: String) -> std::result::Result<(), String> { + Ok(()) + } + fn list_callback_uris(&self) -> std::result::Result, String> { + Ok(BTreeSet::new()) + } + fn get_active_callback_uri(&self) -> std::result::Result { + Ok(String::new()) + } + fn get_next_callback_uri(&self) -> std::result::Result { + Ok(String::new()) + } + fn add_callback_uri(&self, _uri: String) -> std::result::Result<(), String> { + Ok(()) + } + fn remove_callback_uri(&self, _uri: String) -> std::result::Result<(), String> { + Ok(()) + } +} + +#[tokio::test] +async fn test_task_syntax_error() { + let agent = Arc::new(MockAgent::new()); + let task_id = 404; + let code = "print(\"Hello\nprint(\"World\")"; // missing closing quote + + let task = c2::Task { + id: task_id, + tome: Some(Tome { + eldritch: code.to_string(), + ..Default::default() + }), + quest_name: "syntax_error_test".to_string(), + ..Default::default() + }; + + let registry = TaskRegistry::new(); + registry.spawn(task, agent.clone()); + + tokio::time::sleep(Duration::from_secs(3)).await; + + let reports = agent.output_reports.lock().unwrap(); + + // Check for error report + let error_report = reports.iter().find(|r| { + if let Some(report_output_request::Message::TaskOutput(m)) = &r.message { + if let Some(o) = &m.output { + return o.error.is_some(); + } + } + false + }); + + assert!(error_report.is_some(), "Should report error"); + if let Some(r) = error_report { + if let Some(report_output_request::Message::TaskOutput(m)) = &r.message { + if let Some(o) = &m.output { + if let Some(e) = &o.error { + println!("Error message: {}", e.msg); + assert!(e.msg.contains("Error") || e.msg.contains("Lexer") || e.msg.contains("Syntax")); + } + } + } + } +} diff --git a/implants/lib/eldritch/eldritch-core/src/interpreter/core.rs b/implants/lib/eldritch/eldritch-core/src/interpreter/core.rs index a1d77719b..8d9bbc282 100644 --- a/implants/lib/eldritch/eldritch-core/src/interpreter/core.rs +++ b/implants/lib/eldritch/eldritch-core/src/interpreter/core.rs @@ -182,7 +182,12 @@ impl Interpreter { // Check for lexer errors first to maintain behavior for token in &tokens { if let TokenKind::Error(msg) = &token.kind { - return Err(format!("Lexer Error: {}", msg)); + let err = EldritchError::new( + EldritchErrorKind::SyntaxError, + &format!("Lexer Error: {}", msg), + token.span, + ); + return Err(self.format_error(input, err)); } }