-
Notifications
You must be signed in to change notification settings - Fork 36
Email Notification Feature for casr-dojo
#258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,10 @@ | |
| use reqwest::{Client, Method, RequestBuilder, Response, Url}; | ||
| use walkdir::WalkDir; | ||
|
|
||
| use lettre::{Message, SmtpTransport, Transport, transport::smtp::authentication::Credentials}; | ||
| use secrecy::{ExposeSecret, Secret}; | ||
| use std::env; | ||
|
|
||
| use std::collections::HashSet; | ||
| use std::collections::hash_map::DefaultHasher; | ||
| use std::fs; | ||
|
|
@@ -221,7 +225,7 @@ | |
| extra_gdb_report: bool, | ||
| product_name: String, | ||
| test_id: i64, | ||
| ) -> Result<()> { | ||
| ) -> Result<FindingInfo> { | ||
| // Create new finding. | ||
| let mut executable = ""; | ||
| if let Some(fname) = Path::new(&report.executable_path).file_name() { | ||
|
|
@@ -366,7 +370,11 @@ | |
| debug!("Uploaded crash seed for finding '{}' with id={}", title, id); | ||
| } | ||
|
|
||
| Ok(()) | ||
| Ok(FindingInfo { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You may just return JSON stored to |
||
| title, | ||
| severity: severity.to_string(), | ||
| id, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -552,6 +560,174 @@ | |
| d | ||
| } | ||
|
|
||
| fn parse_env_var(s: &str) -> Option<&str> { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can hardcore exactly one supported environment variable |
||
| if let Some(stripped) = s.strip_prefix("${").and_then(|s| s.strip_suffix('}')) { | ||
| Some(stripped) | ||
| } else if let Some(stripped) = s.strip_prefix('$') { | ||
| Some(stripped) | ||
| } else { | ||
| None | ||
| } | ||
| } | ||
|
|
||
| struct FindingInfo { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add docs for structures, fields, and functions |
||
| title: String, | ||
| severity: String, | ||
| id: i64, | ||
| } | ||
|
|
||
| struct MailConfig { | ||
| server: String, | ||
| port: u16, | ||
| email: String, | ||
| use_ssl: bool, | ||
| password: Secret<String>, | ||
| recipients: Vec<String>, | ||
| defectdojo_url: String, | ||
| } | ||
|
|
||
| impl MailConfig { | ||
| pub fn new(toml: toml::Table, defectdojo_url: String) -> Result<Option<Self>> { | ||
| if !toml.contains_key("mail") || !toml["mail"].is_table() { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. non-table 'mail' should be an error |
||
| return Ok(None); | ||
| } | ||
|
|
||
| let mail_settings = toml["mail"].as_table().unwrap(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You may just write |
||
|
|
||
| if !mail_settings.contains_key("smtp_server") || !mail_settings["smtp_server"].is_str() { | ||
| bail!("[mail] smtp_server (string) must be specified in TOML"); | ||
| } | ||
| if !mail_settings.contains_key("port") || !mail_settings["port"].is_integer() { | ||
| bail!("[mail] port (integer) must be specified in TOML"); | ||
| } | ||
| if !mail_settings.contains_key("email") || !mail_settings["email"].is_str() { | ||
| bail!("[mail] email (string) must be specified in TOML"); | ||
| } | ||
| if !mail_settings.contains_key("password") || !mail_settings["password"].is_str() | ||
| { | ||
| bail!("[mail] password (string) must be specified in TOML"); | ||
| } | ||
| if !mail_settings.contains_key("recipients") || !mail_settings["recipients"].is_array() { | ||
| bail!("[mail] recipients (array) must be specified in TOML"); | ||
| } | ||
|
|
||
| if let Some(recipients) = mail_settings["recipients"].as_array() { | ||
| for recipient in recipients { | ||
| if !recipient.is_str() { | ||
| bail!("All elements in [mail] recipients must be strings"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| let server = mail_settings["smtp_server"].as_str().unwrap().to_string(); | ||
| let port = mail_settings["port"].as_integer().unwrap() as u16; | ||
| let email = mail_settings["email"].as_str().unwrap().to_string(); | ||
|
|
||
| let use_ssl = mail_settings | ||
| .get("use_ssl") | ||
| .and_then(|v| v.as_bool()) | ||
| .unwrap_or(port == 465); | ||
|
|
||
| if !mail_settings.contains_key("use_ssl") { | ||
| info!( | ||
| "[mail] use_ssl not specified, assuming {} based on port {}", | ||
| use_ssl, port | ||
| ); | ||
| } | ||
|
|
||
| let password = mail_settings["password"].as_str().unwrap().to_string(); | ||
|
|
||
| let password = if let Some(env_var) = parse_env_var(&password) { | ||
| match env::var(env_var) { | ||
| Ok(val) => val, | ||
| Err(e) => { | ||
| warn!("Environment variable '{}' is not set: {}", env_var, e); | ||
| password | ||
| } | ||
| } | ||
| } else { | ||
| warn!( | ||
| "It is recommended to store SMTP passwords in environment variables for security (e.g. smtp_password = \"$SMTP_PASS\")" | ||
| ); | ||
| password | ||
| }; | ||
| let password = Secret::new(password); | ||
|
|
||
| let recipients = mail_settings["recipients"] | ||
| .as_array() | ||
| .unwrap() | ||
| .iter() | ||
| .map(|v| v.as_str().unwrap().to_string()) | ||
| .collect(); | ||
|
|
||
| Ok(Some(Self { | ||
| server, | ||
| port, | ||
| email, | ||
| use_ssl, | ||
| password, | ||
| recipients, | ||
| defectdojo_url, | ||
| })) | ||
| } | ||
|
|
||
| fn create_transport(&self) -> Result<SmtpTransport> { | ||
| let creds = Credentials::new(self.email.clone(), self.password.expose_secret().clone()); | ||
| let builder = if self.use_ssl { | ||
| SmtpTransport::relay(&self.server)? | ||
| } else { | ||
| SmtpTransport::starttls_relay(&self.server)? | ||
| }; | ||
| Ok(builder.credentials(creds).port(self.port).build()) | ||
| } | ||
|
|
||
| pub async fn send_notification( | ||
| &self, | ||
| product_name: &str, | ||
| findings: &[FindingInfo], | ||
| ) -> Result<()> { | ||
| if findings.is_empty() { | ||
| return Ok(()); | ||
| } | ||
|
|
||
| let transport = self.create_transport()?; | ||
| let subject = format!( | ||
| "[DefectDojo] {} new findings for {}", | ||
| findings.len(), | ||
| product_name | ||
| ); | ||
|
|
||
| let mut body = format!( | ||
| "New findings uploaded to DefectDojo for '{}':\n\n", | ||
| product_name | ||
| ); | ||
|
|
||
| for finding in findings { | ||
| let finding_url = format!("{}/finding/{}", self.defectdojo_url, finding.id); | ||
| body.push_str(&format!( | ||
| "- {} (Severity: {})\n View: {}\n\n", | ||
| finding.title, finding.severity, finding_url | ||
| )); | ||
| } | ||
|
|
||
| let mut email_builder = Message::builder().from(self.email.parse()?); | ||
|
|
||
| for recipient in &self.recipients { | ||
| email_builder = email_builder.to(recipient.parse()?); | ||
| } | ||
|
|
||
| let email = email_builder.subject(subject).body(body)?; | ||
|
|
||
| transport.send(&email)?; | ||
| info!( | ||
| "Summary email notification sent to {} recipients: {}", | ||
| self.recipients.len(), | ||
| self.recipients.join(", ") | ||
| ); | ||
| Ok(()) | ||
| } | ||
| } | ||
|
|
||
| #[tokio::main(flavor = "current_thread")] | ||
| async fn main() -> Result<()> { | ||
| let options = clap::Command::new("casr-dojo") | ||
|
|
@@ -896,6 +1072,7 @@ | |
| let extra_gdb_report = new_casr_reports.iter().any(|(_, _, gdb)| gdb.is_some()); | ||
| let mut tasks = tokio::task::JoinSet::new(); | ||
| let mut new_casr_reports = new_casr_reports.into_iter(); | ||
| let mut findings = Vec::new(); | ||
| loop { | ||
| while tasks.len() < CONCURRENCY_LIMIT { | ||
| let Some((path, report, gdb)) = new_casr_reports.next() else { | ||
|
|
@@ -911,10 +1088,37 @@ | |
| let Some(r) = tasks.join_next().await else { | ||
| break; | ||
| }; | ||
| if let Err(e) = r? { | ||
| error!("{}", e); | ||
| match r? { | ||
| Ok(finding) => { | ||
| findings.push(finding); | ||
| } | ||
| Err(e) => { | ||
| error!("{}", e); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| let mail_sender = match MailConfig::new( | ||
| toml.clone(), | ||
| options.get_one::<Url>("url").unwrap().to_string(), | ||
| ) { | ||
| Ok(Some(config)) => config, | ||
| Ok(None) => { | ||
| warn!("Mail configuration not found in TOML, skipping notifications"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There should be no warning here |
||
| return Ok(()); | ||
| } | ||
| Err(e) => { | ||
| error!("Failed to parse mail config: {}", e); | ||
| return Ok(()); | ||
| } | ||
| }; | ||
| info!("Sending email notification for product '{}'", product_name); | ||
| if let Err(e) = mail_sender | ||
| .send_notification(&product_name.to_string(), &findings) | ||
| .await | ||
| { | ||
| error!("Failed to send notification: {}", e); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -613,7 +613,7 @@ your project before (via `dotnet build` or `dotnet publish`) and specify `--no-b | |
| ## casr-libfuzzer | ||
|
|
||
| Triage crashes found by libFuzzer based fuzzer | ||
| (C/C++/go-fuzz/Atheris/Jazzer/Jazzer.js/jsfuzz/luzer) or LibAFL based fuzzer | ||
| (C/C++/go-fuzz/Atheris/Jazzer/Jazzer.js/jsfuzz) or LibAFL based fuzzer | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. redundant change |
||
|
|
||
| Usage: casr-libfuzzer [OPTIONS] --output <OUTPUT_DIR> -- <ARGS>... | ||
|
|
||
|
|
@@ -770,6 +770,17 @@ name = "load_fuzzer 2023-06-07T16:47:18+03:00" | |
| [test] | ||
| test_type = "CASR DAST Report" | ||
| ``` | ||
| Also `casr-dojo` can send email notifications about newly uploaded findings. To enable this feature, add a `[mail]` section to your TOML configuration file: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add newline here |
||
|
|
||
| ```toml | ||
| [mail] | ||
| smtp_server = "your.smtp.server.com" # SMTP server | ||
| port = 465 # SMTP port | ||
| use_ssl = true # Whether to use SSL (default: false, except when port is 465) | ||
| email = "sender@example.com" # Sender email address | ||
| password = "$ENV_VAR_NAME" # Password for the email address (can use environment variables with $ prefix) | ||
| recipients = ["recipient1@example.com", "recipient2@example.com"] # List of recipients to send the notification to | ||
| ``` | ||
|
|
||
| CASR must be built with `dojo` feature via `cargo install -F dojo casr` or | ||
| `cargo build -F dojo --release`. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, make these dependencies available only when casr is built with
dojofeature (similar to tokio crate).