diff --git a/Cargo.lock b/Cargo.lock index c0f0a93..612f200 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,6 +80,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" + [[package]] name = "async-trait" version = "0.1.77" @@ -845,6 +851,7 @@ dependencies = [ name = "office_audit_log_collector" version = "2.5.0" dependencies = [ + "anyhow", "async-trait", "base64 0.22.0", "chrono", @@ -860,6 +867,7 @@ dependencies = [ "serde_json", "serde_yaml", "sha2", + "simple-logging", "simple_logger", "tokio", "tokio-stream", @@ -933,7 +941,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "smallvec", "windows-targets 0.48.5", ] @@ -1045,6 +1053,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1264,6 +1278,17 @@ dependencies = [ "libc", ] +[[package]] +name = "simple-logging" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00d48e85675326bb182a2286ea7c1a0b264333ae10f27a937a72be08628b542" +dependencies = [ + "lazy_static", + "log", + "thread-id", +] + [[package]] name = "simple_logger" version = "4.3.3" @@ -1363,6 +1388,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "thread-id" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1" +dependencies = [ + "libc", + "redox_syscall 0.1.57", + "winapi", +] + [[package]] name = "time" version = "0.3.34" @@ -1669,6 +1705,28 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index c1a3d68..65f04f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] +anyhow = "1.0.81" log = "0.4.16" simple_logger = "4.3.3" chrono = "0.4.19" @@ -23,3 +24,4 @@ base64 = "0.22.0" hmac = "0.12.1" sha2 = "0.10.8" async-trait = "0.1.77" +simple-logging = "2.0.2" diff --git a/Release/Linux/OfficeAuditLogCollector b/Release/Linux/OfficeAuditLogCollector old mode 100755 new mode 100644 index 49e3cee..0f27a41 Binary files a/Release/Linux/OfficeAuditLogCollector and b/Release/Linux/OfficeAuditLogCollector differ diff --git a/Release/Windows/OfficeAuditLogCollector.exe b/Release/Windows/OfficeAuditLogCollector.exe index 2abb857..0dcbfdf 100644 Binary files a/Release/Windows/OfficeAuditLogCollector.exe and b/Release/Windows/OfficeAuditLogCollector.exe differ diff --git a/src/api_connection.rs b/src/api_connection.rs index a7c0e7f..9979b85 100644 --- a/src/api_connection.rs +++ b/src/api_connection.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use reqwest; -use log::{debug, warn, error}; +use log::{debug, warn, error, info}; use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap}; use tokio; use serde_json; @@ -9,17 +9,19 @@ use futures::channel::mpsc::{Receiver, Sender}; use crate::config::Config; use crate::data_structures::{JsonList, StatusMessage, GetBlobConfig, GetContentConfig, AuthResult, ContentToRetrieve, CliArgs}; +use anyhow::Result; +use serde_json::Value; /// Return a logged in API connection object. Use the Headers value to make API requests. -pub fn get_api_connection(args: CliArgs, config: Config) -> ApiConnection { +pub async fn get_api_connection(args: CliArgs, config: Config) -> ApiConnection { let mut api = ApiConnection { args, config, headers: HeaderMap::new(), }; - api.login(); + api.login().await; api } @@ -34,7 +36,8 @@ pub struct ApiConnection { impl ApiConnection { /// Use tenant_id, client_id and secret_key to request a bearer token and store it in /// our headers. Must be called once before requesting any content. - fn login(&mut self) { + async fn login(&mut self) { + info!("Logging in to Office Management API."); let auth_url = format!("https://login.microsoftonline.com/{}/oauth2/token", self.args.tenant_id.to_string()); @@ -48,43 +51,113 @@ impl ApiConnection { self.headers.insert(CONTENT_TYPE, "application/x-www-form-urlencoded".parse().unwrap()); - let login_client = reqwest::blocking::Client::new(); - let json: AuthResult = login_client + let login_client = reqwest::Client::new(); + let result = login_client .post(auth_url) .headers(self.headers.clone()) .form(¶ms) .send() - .unwrap_or_else(|e| panic!("Could not send API login request: {}", e)) - .json() - .unwrap_or_else(|e| panic!("Could not parse API login reply: {}", e)); + .await; + let response = match result { + Ok(response) => response, + Err(e) => { + let msg = format!("Could not send API login request: {}", e); + error!("{}", msg); + panic!("{}", msg); + } + }; + if !response.status().is_success() { + let text = match response.text().await { + Ok(text) => text, + Err(e) => { + let msg = format!("Received error response to API login, but could not parse response: {}", e); + error!("{}", msg); + panic!("{}", msg); + } + }; + let msg = format!("Received error response to API login: {}", text); + error!("{}", msg); + panic!("{}", msg); + } + let json = match response.json::().await { + Ok(json) => json, + Err(e) => { + let msg = format!("Could not parse API login reply: {}", e); + error!("{}", msg); + panic!("{}", msg); + } + }; let token = format!("bearer {}", json.access_token); self.headers.insert(AUTHORIZATION, token.parse().unwrap()); + info!("Successfully logged in to Office Management API.") } fn get_base_url(&self) -> String { format!("https://manage.office.com/api/v1.0/{}/activity/feed", self.args.tenant_id) } - pub fn subscribe_to_feeds(&self) { + pub async fn subscribe_to_feeds(&self) -> Result<()> { - let content_types = self.config.collect.content_types.get_content_type_strings(); + info!("Subscribing to audit feeds."); + let mut content_types = self.config.collect.content_types.get_content_type_strings(); - let client = reqwest::blocking::Client::new(); + let client = reqwest::Client::new(); + info!("Getting current audit feed subscriptions."); + let url = format!("{}/subscriptions/list", self.get_base_url()); + let result: Vec> = client + .get(url) + .headers(self.headers.clone()) + .header("content-length", 0) + .send() + .await? + .json() + .await?; + for subscription in result { + let status = subscription + .get("status") + .expect("No status in JSON") + .as_str() + .unwrap() + .to_string() + .to_lowercase(); + if status == "enabled" { + let content_type = subscription + .get("contentType") + .expect("No contentType in JSON") + .as_str() + .unwrap() + .to_string() + .to_lowercase(); + if let Some(i) = content_types + .iter() + .position(|x| x.to_lowercase() == content_type) { + info!("Already subscribed to feed {}", content_type); + content_types.remove(i); + } + } + } for content_type in content_types { let url = format!("{}/subscriptions/start?contentType={}", self.get_base_url(), content_type ); - client + debug!("Subscribing to {} feed.", content_type); + let response = client .post(url) .headers(self.headers.clone()) .header("content-length", 0) .send() - .unwrap_or_else( - |e| panic!("Error setting feed subscription status {}", e) - ); + .await?; + if !response.status().is_success() { + let text = response.text().await?; + let msg = format!("Received error response subscribing to audit feed {}: {}", content_type, text); + error!("{}", msg); + panic!("{}", msg); + } } + info!("All audit feeds subscriptions exist."); + Ok(()) } diff --git a/src/collector.rs b/src/collector.rs index e61c312..f836a0d 100644 --- a/src/collector.rs +++ b/src/collector.rs @@ -43,7 +43,7 @@ pub struct Collector { impl Collector { - pub fn new(args: CliArgs, config: Config, runs: HashMap>) -> Collector { + pub async fn new(args: CliArgs, config: Config, runs: HashMap>) -> Collector { // Initialize interfaces let mut interfaces: Vec> = Vec::new(); @@ -63,8 +63,12 @@ impl Collector { // Initialize collector threads let api = api_connection::get_api_connection( args.clone(), config.clone() - ); - api.subscribe_to_feeds(); + ).await; + if let Err(e) = api.subscribe_to_feeds().await { + let msg = format!("Error subscribing to audit feeds: {}", e); + error!("{}", msg); + panic!("{}", msg); + } let known_blobs = config.load_known_blobs(); let (result_rx, stats_rx, kill_tx) = diff --git a/src/interfaces/azure_oms_interface.rs b/src/interfaces/azure_oms_interface.rs index 9e93d86..cc4167f 100644 --- a/src/interfaces/azure_oms_interface.rs +++ b/src/interfaces/azure_oms_interface.rs @@ -4,7 +4,7 @@ use base64::prelude::BASE64_STANDARD; use chrono::Utc; use futures::{stream, StreamExt}; use hmac::{Hmac, Mac}; -use log::warn; +use log::{error, info, warn}; use sha2::Sha256; use crate::config::Config; use crate::data_structures::Caches; @@ -56,7 +56,7 @@ impl Interface for OmsInterface { async fn send_logs(&mut self, logs: Caches) { let client = reqwest::Client::new(); - println!("SEND"); + info!("Sending logs to OMS interface."); let mut requests = Vec::new(); for (content_type, content_logs) in logs.get_all_types() { for log in content_logs.iter() { @@ -75,33 +75,47 @@ impl Interface for OmsInterface { } } + let resource = "/api/logs"; + let uri = format!("https://{}.ods.opinsights.azure.com{}?api-version=2016-04-01", + self.config.output.oms.as_ref().unwrap().workspace_id, resource); + + info!("URL for OMS calls will be: {}", uri); let calls = stream::iter(requests) .map(|(body, table_name, time_value, content_length)| { let client = client.clone(); - let rfc1123date = Utc::now().format("%a, %d %b %Y %H:%M:%S GMT"); + let uri = uri.clone(); let method = "POST".to_string(); let content_type = "application/json".to_string(); - let resource = "/api/logs".to_string(); - let signature = self.build_signature(rfc1123date.to_string(), content_length, - method.clone(), content_type.to_string(), + let rfc1123date = Utc::now().format("%a, %d %b %Y %H:%M:%S GMT").to_string(); + let signature = self.build_signature(rfc1123date.clone(), content_length, + method.clone(), content_type.clone(), resource.to_string()); - - let uri = format!("https://{}.ods.opinsights.azure.com{}?api-version=2016-04-01", - self.config.output.oms.as_ref().unwrap().workspace_id, resource); tokio::spawn(async move { - let resp = client + let result = client .post(uri) .header("content-type", "application/json") .header("content-length", content_length) .header("Authorization", signature) .header("Log-Type", table_name) - .header("x-ms-date", rfc1123date.to_string()) + .header("x-ms-date", rfc1123date.clone()) .header("time-generated-field", time_value) .body(body) .send() - .await.unwrap(); - resp.bytes().await + .await; + match result { + Ok(response) => { + if !response.status().is_success() { + match response.text().await { + Ok(text) => error!("Error response after sending log to OMS: {}", text), + Err(e) => error!("Error response after sending log to OMS, but could not parse response: {}", e), + } + } + }, + Err(e) => { + error!("Error send log to OMS: {}", e); + } + } }) }) .buffer_unordered(10); diff --git a/src/main.rs b/src/main.rs index 49ee215..29e4d02 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use clap::Parser; use crate::collector::Collector; use crate::config::Config; +use log::LevelFilter; mod collector; mod api_connection; @@ -14,7 +15,24 @@ async fn main() { let args = data_structures::CliArgs::parse(); let config = Config::new(args.config.clone()); + init_logging(&config); + let runs = config.get_needed_runs(); - let mut collector = Collector::new(args, config, runs); + let mut collector = Collector::new(args, config, runs).await; collector.monitor().await; } + +fn init_logging(config: &Config) { + + let (path, level) = if let Some(log_config) = &config.log { + let level = if log_config.debug { LevelFilter::Debug } else { LevelFilter::Info }; + (log_config.path.clone(), level) + } else { + ("".to_string(), LevelFilter::Info) + }; + if !path.is_empty() { + simple_logging::log_to_file(path, level).unwrap(); + } else { + simple_logging::log_to_stderr(level); + } +}