diff --git a/Cargo.toml b/Cargo.toml index 723e204..ec9f778 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "security_log_analysis_rust" -version = "0.11.4" +version = "0.11.5" authors = ["Daniel Boline "] edition = "2018" @@ -14,7 +14,7 @@ Analyze Auth Logs.""" [dependencies] anyhow = "1.0" -authorized_users = { git = "https://github.com/ddboline/auth_server_rust.git", tag="0.11.5"} +authorized_users = { git = "https://github.com/ddboline/auth_server_rust.git", tag="0.11.7"} aws-config = {version="1.0", features=["behavior-version-latest"]} aws-sdk-s3 = "1.1" aws-sdk-ses = "1.1" @@ -48,6 +48,7 @@ refinery = {version="0.8", features=["tokio-postgres"]} reqwest = {version="0.11", features=["json", "rustls-tls"], default_features=false} serde = { version="1.0", features=["derive"]} serde_json = "1.0" +serde_yaml = "0.9" smallvec = "1.6" stack-string = { git = "https://github.com/ddboline/stack-string-rs.git", features=["postgres_types", "rweb-openapi"], tag="0.9.2" } stdout-channel = "0.6" @@ -57,7 +58,7 @@ time-tz = {version="2.0", features=["system"]} tokio-postgres = {version="0.7", features=["with-time-0_3", "with-uuid-1", "with-serde_json-1"]} tokio = {version="1.34", features=["rt", "macros", "rt-multi-thread"]} rweb = {git = "https://github.com/ddboline/rweb.git", features=["openapi"], default-features=false, tag="0.15.1-1"} -rweb-helper = { git = "https://github.com/ddboline/rweb_helper.git", tag="0.5.0-1" } +rweb-helper = { git = "https://github.com/ddboline/rweb_helper.git", tag="0.5.1" } uuid = { version = "1.0", features = ["serde", "v4"] } [[bin]] diff --git a/scripts/openapi.yaml b/scripts/openapi.yaml new file mode 100644 index 0000000..e505ce9 --- /dev/null +++ b/scripts/openapi.yaml @@ -0,0 +1,472 @@ +openapi: 3.0.1 +info: + title: Frontend for AWS + description: Web Frontend for AWS Services + version: 0.11.5 +paths: + /security_log/intrusion_attempts: + get: + parameters: + - name: service + in: query + required: false + schema: + nullable: true + type: string + enum: + - Apache + - Nginx + - Ssh + - name: location + in: query + required: false + schema: + nullable: true + type: string + enum: + - Home + - Cloud + - name: ndays + in: query + required: false + schema: + nullable: true + type: integer + responses: + '200': + description: Intrusion Attempts + content: + text/html: + schema: + type: string + '400': + description: Bad Request + '404': + description: Not Found + '500': + description: Internal Server Error + /security_log/map_script.js: + get: + responses: + '200': + description: Map Drawing Script + content: + text/javascript: + schema: + type: string + /security_log/intrusion_attempts/all: + get: + parameters: + - name: service + in: query + required: false + schema: + nullable: true + type: string + enum: + - Apache + - Nginx + - Ssh + - name: location + in: query + required: false + schema: + nullable: true + type: string + enum: + - Home + - Cloud + - name: ndays + in: query + required: false + schema: + nullable: true + type: integer + responses: + '200': + description: All Intrusion Attempts + content: + text/html: + schema: + type: string + '400': + description: Bad Request + '404': + description: Not Found + '500': + description: Internal Server Error + /security_log/intrusion_log: + get: + parameters: + - name: service + in: query + required: false + schema: + nullable: true + type: string + enum: + - Apache + - Nginx + - Ssh + - name: server + in: query + required: false + schema: + nullable: true + type: string + enum: + - Home + - Cloud + - name: offset + in: query + required: false + schema: + nullable: true + type: integer + minimum: 0 + - name: limit + in: query + required: false + schema: + nullable: true + type: integer + minimum: 0 + responses: + '200': + description: Intrusion Logs + content: + application/json: + schema: + items: + $ref: '#/components/schemas/IntrusionLog' + type: array + '400': + description: Bad Request + '404': + description: Not Found + '500': + description: Internal Server Error + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IntrusionLogUpdate' + required: true + responses: + '201': + description: Intrusion Log Post + content: + text/plain: + schema: + type: string + '400': + description: Bad Request + '404': + description: Not Found + '500': + description: Internal Server Error + /security_log/host_country: + get: + parameters: + - name: offset + in: query + required: false + schema: + nullable: true + type: integer + minimum: 0 + - name: limit + in: query + required: false + schema: + nullable: true + type: integer + minimum: 0 + responses: + '200': + description: Host Countries + content: + application/json: + schema: + items: + $ref: '#/components/schemas/HostCountry' + type: array + '400': + description: Bad Request + '404': + description: Not Found + '500': + description: Internal Server Error + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/HostCountryUpdate' + required: true + responses: + '201': + description: Host Country Post + content: + text/plain: + schema: + type: string + '400': + description: Bad Request + '404': + description: Not Found + '500': + description: Internal Server Error + /security_log/cleanup: + post: + responses: + '201': + description: Host Country Cleanup + content: + application/json: + schema: + items: + $ref: '#/components/schemas/HostCountry' + type: array + '400': + description: Bad Request + '404': + description: Not Found + '500': + description: Internal Server Error + /security_log/user: + get: + responses: + '200': + description: Logged User + content: + application/json: + schema: + $ref: '#/components/schemas/LoggedUser' + '400': + description: Bad Request + '404': + description: Not Found + '500': + description: Internal Server Error + /security_log/log_messages: + get: + parameters: + - name: log_level + in: query + required: false + schema: + nullable: true + type: string + enum: + - Debug + - Info + - Warning + - Error + - name: log_unit + in: query + required: false + schema: + nullable: true + type: string + - name: min_date + in: query + required: false + schema: + format: date-time + nullable: true + type: string + - name: max_date + in: query + required: false + schema: + format: date-time + nullable: true + type: string + - name: limit + in: query + required: false + schema: + nullable: true + type: integer + minimum: 0 + - name: offset + in: query + required: false + schema: + nullable: true + type: integer + minimum: 0 + responses: + '200': + description: Log Messages + content: + application/json: + schema: + items: + $ref: '#/components/schemas/SystemdLogMessages' + type: array + '400': + description: Bad Request + '404': + description: Not Found + '500': + description: Internal Server Error + /security_log/log_messages/{id}: + delete: + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: Delete Log Messages + content: + text/plain: + schema: + type: string + '400': + description: Bad Request + '404': + description: Not Found + '500': + description: Internal Server Error +components: + schemas: + IntrusionLog: + properties: + id: + format: uuid + example: 334518f4-1bfd-4f20-9978-bfad0dc033e1 + type: string + service: + type: string + server: + type: string + datetime: + format: date-time + type: string + host: + type: string + username: + nullable: true + type: string + type: object + required: + - id + - service + - server + - datetime + - host + IntrusionLogUpdate: + properties: + updates: + items: + $ref: '#/components/schemas/IntrusionLog' + type: array + type: object + required: + - updates + HostCountry: + properties: + host: + description: Host + type: string + code: + description: Country Code + type: string + ipaddr: + description: IP Address + nullable: true + type: string + created_at: + description: Created At + format: date-time + type: string + type: object + required: + - host + - code + - created_at + HostCountryUpdate: + properties: + updates: + items: + properties: + host: + type: string + code: + type: string + ipaddr: + nullable: true + type: string + created_at: + format: date-time + type: string + type: object + required: + - host + - code + - created_at + type: array + type: object + required: + - updates + LoggedUser: + properties: + email: + description: Email Address + type: string + session: + description: Session Id + format: uuid + example: 334518f4-1bfd-4f20-9978-bfad0dc033e1 + type: string + secret_key: + description: Secret Key + type: string + type: object + required: + - email + - session + - secret_key + SystemdLogMessages: + properties: + id: + description: ID + format: uuid + example: 334518f4-1bfd-4f20-9978-bfad0dc033e1 + type: string + log_level: + description: Log Level + type: string + enum: + - Debug + - Info + - Warning + - Error + log_unit: + description: Log Unit + nullable: true + type: string + log_message: + description: Log Message + type: string + log_timestamp: + description: Log Timestamp + format: date-time + type: string + processed_time: + description: Log Processed At Time + format: date-time + nullable: true + type: string + type: object + required: + - id + - log_level + - log_message + - log_timestamp diff --git a/src/errors.rs b/src/errors.rs index 8fffe37..9ffb04d 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,6 +1,14 @@ use anyhow::Error as AnyhowError; +use log::error; use postgres_query::Error as PgError; -use rweb::reject::Reject; +use rweb::{ + http::StatusCode, + openapi::{ + ComponentDescriptor, ComponentOrInlineSchema, Entity, Response, ResponseEntity, Responses, + }, + reject::Reject, +}; +use std::borrow::Cow; use thiserror::Error; use tokio::task::JoinError; @@ -17,3 +25,36 @@ pub enum ServiceError { } impl Reject for ServiceError {} + +impl Entity for ServiceError { + fn type_name() -> Cow<'static, str> { + rweb::http::Error::type_name() + } + fn describe(comp_d: &mut ComponentDescriptor) -> ComponentOrInlineSchema { + rweb::http::Error::describe(comp_d) + } +} + +impl ResponseEntity for ServiceError { + fn describe_responses(_: &mut ComponentDescriptor) -> Responses { + let mut map = Responses::new(); + + let error_responses = [ + (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error"), + (StatusCode::BAD_REQUEST, "Bad Request"), + (StatusCode::NOT_FOUND, "Not Found"), + ]; + + for (code, msg) in &error_responses { + map.insert( + Cow::Owned(code.as_str().into()), + Response { + description: Cow::Borrowed(*msg), + ..Response::default() + }, + ); + } + + map + } +} diff --git a/src/logged_user.rs b/src/logged_user.rs index 9ffe1bf..6913708 100644 --- a/src/logged_user.rs +++ b/src/logged_user.rs @@ -1,6 +1,6 @@ pub use authorized_users::{ get_random_key, get_secrets, token::Token, AuthorizedUser, AUTHORIZED_USERS, JWT_SECRET, - KEY_LENGTH, SECRET_KEY, TRIGGER_DB_UPDATE, + KEY_LENGTH, LOGIN_HTML, SECRET_KEY, TRIGGER_DB_UPDATE, }; use futures::TryStreamExt; use log::debug; @@ -22,6 +22,7 @@ use uuid::Uuid; use crate::{errors::ServiceError as Error, models::AuthorizedUsers, pgpool::PgPool}; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Schema)] +#[schema(component = "LoggedUser")] pub struct LoggedUser { #[schema(description = "Email Address")] pub email: StackString, diff --git a/src/models.rs b/src/models.rs index d5739d7..9cffe68 100644 --- a/src/models.rs +++ b/src/models.rs @@ -36,7 +36,7 @@ impl CountryCode { } } -#[derive(FromSqlRow, Clone, Debug, Serialize, Deserialize, Schema)] +#[derive(FromSqlRow, Clone, Debug, Serialize, Deserialize, Schema, PartialEq)] pub struct HostCountry { pub host: StackString, pub code: StackString, @@ -520,7 +520,7 @@ impl ToSql for LogLevel { } } -#[derive(FromSqlRow, Clone, Debug, Serialize, Deserialize)] +#[derive(FromSqlRow, Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct SystemdLogMessages { pub id: Uuid, pub log_level: LogLevel, diff --git a/src/security_log_element.rs b/src/security_log_element.rs index de7719c..4485066 100644 --- a/src/security_log_element.rs +++ b/src/security_log_element.rs @@ -1,6 +1,6 @@ use dioxus::prelude::{ - dioxus_elements, format_args_f, component, rsx, Element, GlobalAttributes, LazyNodes, Props, - Scope, VNode, VirtualDom, IntoDynNode, + component, dioxus_elements, format_args_f, rsx, Element, GlobalAttributes, IntoDynNode, + LazyNodes, Props, Scope, VNode, VirtualDom, }; use stack_string::StackString; use std::fmt::Write; diff --git a/src/security_log_http.rs b/src/security_log_http.rs index 7e6b6cc..cc3d4c2 100644 --- a/src/security_log_http.rs +++ b/src/security_log_http.rs @@ -25,13 +25,25 @@ use futures::TryStreamExt; use itertools::Itertools; use log::error; use rweb::{ - delete, get, http::StatusCode, post, reject::Reject, Filter, Json, Query, Rejection, Reply, - Schema, + delete, + filters::BoxedFilter, + get, + http::{header::CONTENT_TYPE, StatusCode}, + openapi, + openapi::Info, + post, + reject::{InvalidHeader, MissingCookie, Reject}, + Filter, Json, Query, Rejection, Reply, Schema, +}; +use rweb_helper::{ + derive_rweb_schema, html_response::HtmlResponse as HtmlBase, + json_response::JsonResponse as JsonBase, DateTimeType, RwebResponse, UuidWrapper, }; -use rweb_helper::{derive_rweb_schema, DateTimeType, UuidWrapper}; use serde::{Deserialize, Serialize}; use stack_string::{format_sstr, StackString}; -use std::{convert::Infallible, env::var, fmt, fmt::Write, net::SocketAddr, time::Duration}; +use std::{ + convert::Infallible, env::var, fmt, fmt::Write, net::SocketAddr, sync::Arc, time::Duration, +}; use thiserror::Error; use time::OffsetDateTime; use tokio::{ @@ -43,7 +55,7 @@ use security_log_analysis_rust::{ config::Config, errors::ServiceError, host_country_metadata::HostCountryMetadata, - logged_user::{fill_from_db, get_secrets, LoggedUser, TRIGGER_DB_UPDATE}, + logged_user::{fill_from_db, get_secrets, LoggedUser, LOGIN_HTML, TRIGGER_DB_UPDATE}, models::{HostCountry, IntrusionLog, LogLevel, SystemdLogMessages}, parse_logs::{parse_systemd_logs_sshd_daemon, process_systemd_logs}, pgpool::PgPool, @@ -60,6 +72,10 @@ struct ErrorMessage<'a> { message: &'a str, } +fn login_html() -> impl Reply { + rweb::reply::html(LOGIN_HTML) +} + /// # Errors /// Never returns error #[allow(clippy::unused_async)] @@ -70,6 +86,16 @@ pub async fn error_response(err: Rejection) -> Result, Infallible if err.is_not_found() { code = StatusCode::NOT_FOUND; message = "NOT FOUND"; + } else if err.find::().is_some() { + TRIGGER_DB_UPDATE.set(); + return Ok(Box::new(login_html())); + } else if let Some(missing_cookie) = err.find::() { + if missing_cookie.name() == "jwt" { + TRIGGER_DB_UPDATE.set(); + return Ok(Box::new(login_html())); + } + code = StatusCode::INTERNAL_SERVER_ERROR; + message = "Internal Server Error"; } else if let Some(service_error) = err.find::() { error!("{:?}", service_error); code = StatusCode::INTERNAL_SERVER_ERROR; @@ -144,22 +170,30 @@ async fn get_cached_country_count( Ok(body) } +#[derive(RwebResponse)] +#[response(description = "Map Drawing Script", content = "js")] +struct MapScriptResponse(HtmlBase<&'static str, Infallible>); + #[get("/security_log/map_script.js")] -async fn map_script() -> WarpResult { +async fn map_script() -> WarpResult { let body = include_str!("../templates/map_script.js"); - Ok(rweb::reply::html(body)) + Ok(HtmlBase::new(body).into()) } +#[derive(RwebResponse)] +#[response(description = "Intrusion Attempts", content = "html")] +struct IntrusionAttemptsResponse(HtmlBase); + #[get("/security_log/intrusion_attempts")] async fn intrusion_attempts( query: Query, #[data] data: AppState, -) -> WarpResult { +) -> WarpResult { let query = query.into_inner(); let config = data.config.clone(); let data = get_cached_country_count(&data.pool, query).await?; let body = security_log_element::index_body(data, config); - Ok(rweb::reply::html(body)) + Ok(HtmlBase::new(body.into()).into()) } #[cached( @@ -190,16 +224,20 @@ async fn get_cached_country_count_all( Ok(body) } +#[derive(RwebResponse)] +#[response(description = "All Intrusion Attempts", content = "html")] +struct IntrusionAttemptsAllResponse(HtmlBase); + #[get("/security_log/intrusion_attempts/all")] async fn intrusion_attempts_all( query: Query, #[data] data: AppState, -) -> WarpResult { +) -> WarpResult { let query = query.into_inner(); let config = data.config.clone(); let data = get_cached_country_count_all(config.clone(), query).await?; let body = security_log_element::index_body(data, config); - Ok(rweb::reply::html(body)) + Ok(HtmlBase::new(body.into()).into()) } #[derive(Serialize, Deserialize, Schema)] @@ -210,12 +248,16 @@ struct SyncQuery { limit: Option, } +#[derive(RwebResponse)] +#[response(description = "Intrusion Logs")] +struct IntrusionLogResponse(JsonBase, ServiceError>); + #[get("/security_log/intrusion_log")] async fn intursion_log_get( query: Query, #[data] data: AppState, _: LoggedUser, -) -> WarpResult { +) -> WarpResult { let query = query.into_inner(); let limit = query.limit.unwrap_or(1000); let results: Vec<_> = IntrusionLog::get_intrusion_log_filtered( @@ -229,10 +271,11 @@ async fn intursion_log_get( ) .await .map_err(Into::::into)? + .map_ok(Into::into) .try_collect() .await .map_err(Into::::into)?; - Ok(rweb::reply::json(&results)) + Ok(JsonBase::new(results).into()) } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Into, From)] @@ -242,6 +285,7 @@ derive_rweb_schema!(IntrusionLogWrapper, _IntrusionLogWrapper); #[allow(dead_code)] #[derive(Schema)] +#[schema(component = "IntrusionLog")] struct _IntrusionLogWrapper { id: UuidWrapper, service: StackString, @@ -252,22 +296,46 @@ struct _IntrusionLogWrapper { } #[derive(Serialize, Deserialize, Schema)] +#[schema(component = "IntrusionLogUpdate")] struct IntrusionLogUpdate { updates: Vec, } +#[derive(RwebResponse)] +#[response(description = "Intrusion Log Post", status = "CREATED")] +struct IntrusionLogPostResponse(HtmlBase); + #[post("/security_log/intrusion_log")] async fn intrusion_log_post( payload: Json, #[data] data: AppState, _: LoggedUser, -) -> WarpResult { +) -> WarpResult { let payload = payload.into_inner(); let updates: Vec<_> = payload.updates.into_iter().map(Into::into).collect(); let inserts = IntrusionLog::insert(&data.pool, &updates) .await .map_err(Into::::into)?; - Ok(rweb::reply::html(format_sstr!("Inserts {}", inserts))) + Ok(HtmlBase::new(format_sstr!("Inserts {}", inserts)).into()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Into, From)] +struct HostCountryWrapper(HostCountry); + +derive_rweb_schema!(HostCountryWrapper, _HostCountryWrapper); + +#[allow(dead_code)] +#[derive(Schema)] +#[schema(component = "HostCountry")] +struct _HostCountryWrapper { + #[schema(description = "Host")] + pub host: StackString, + #[schema(description = "Country Code")] + pub code: StackString, + #[schema(description = "IP Address")] + pub ipaddr: Option, + #[schema(description = "Created At")] + pub created_at: DateTimeType, } #[derive(Serialize, Deserialize, Schema)] @@ -276,35 +344,45 @@ struct HostCountryQuery { limit: Option, } +#[derive(RwebResponse)] +#[response(description = "Host Countries")] +struct HostCountryResponse(JsonBase, ServiceError>); + #[get("/security_log/host_country")] async fn host_country_get( query: Query, #[data] data: AppState, _: LoggedUser, -) -> WarpResult { +) -> WarpResult { let query = query.into_inner(); let limit = query.limit.unwrap_or(1000); let results: Vec<_> = HostCountry::get_host_country(&data.pool, query.offset, Some(limit), true) .await .map_err(Into::::into)? + .map_ok(Into::into) .try_collect() .await .map_err(Into::::into)?; - Ok(rweb::reply::json(&results)) + Ok(JsonBase::new(results).into()) } #[derive(Serialize, Deserialize, Schema)] +#[schema(component = "HostCountryUpdate")] struct HostCountryUpdate { updates: Vec, } +#[derive(RwebResponse)] +#[response(description = "Host Country Post", status = "CREATED")] +struct HostCountryPostResponse(HtmlBase); + #[post("/security_log/host_country")] async fn host_country_post( payload: Json, #[data] data: AppState, _: LoggedUser, -) -> WarpResult { +) -> WarpResult { let payload = payload.into_inner(); let mut inserts = 0; for entry in payload.updates { @@ -314,11 +392,18 @@ async fn host_country_post( .map_err(Into::::into)? .map_or(0, |_| 1); } - Ok(rweb::reply::html(format_sstr!("Inserts {inserts}"))) + Ok(HtmlBase::new(format_sstr!("Inserts {inserts}")).into()) } -#[get("/security_log/cleanup")] -async fn host_country_cleanup(#[data] data: AppState, _: LoggedUser) -> WarpResult { +#[derive(RwebResponse)] +#[response(description = "Host Country Cleanup", status = "CREATED")] +struct HostCountryCleanupResponse(JsonBase, ServiceError>); + +#[post("/security_log/cleanup")] +async fn host_country_cleanup( + #[data] data: AppState, + _: LoggedUser, +) -> WarpResult { let mut lines = Vec::new(); let metadata = HostCountryMetadata::from_pool(data.pool.clone()) .await @@ -336,16 +421,20 @@ async fn host_country_cleanup(#[data] data: AppState, _: LoggedUser) -> WarpResu HostCountry::insert_host_country(&host_country, &data.pool) .await .map_err(Into::::into)?; - lines.push(host_country); + lines.push(host_country.into()); } } - Ok(rweb::reply::json(&lines)) + Ok(JsonBase::new(lines).into()) } +#[derive(RwebResponse)] +#[response(description = "Logged User")] +struct LoggedUserResponse(JsonBase); + #[get("/security_log/user")] #[allow(clippy::unused_async)] -async fn user(user: LoggedUser) -> WarpResult { - Ok(rweb::reply::json(&user)) +async fn user(user: LoggedUser) -> WarpResult { + Ok(JsonBase::new(user).into()) } #[derive(Serialize, Deserialize, Schema)] @@ -358,12 +447,39 @@ struct LogMessageQuery { offset: Option, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Into, From)] +struct SystemdLogMessagesWrapper(SystemdLogMessages); + +derive_rweb_schema!(SystemdLogMessagesWrapper, _SystemdLogMessagesWrapper); + +#[allow(dead_code)] +#[derive(Schema)] +#[schema(component = "SystemdLogMessages")] +struct _SystemdLogMessagesWrapper { + #[schema(description = "ID")] + id: UuidWrapper, + #[schema(description = "Log Level")] + log_level: LogLevel, + #[schema(description = "Log Unit")] + log_unit: Option, + #[schema(description = "Log Message")] + log_message: StackString, + #[schema(description = "Log Timestamp")] + log_timestamp: DateTimeType, + #[schema(description = "Log Processed At Time")] + processed_time: Option, +} + +#[derive(RwebResponse)] +#[response(description = "Log Messages")] +struct LogMessagesResponse(JsonBase, ServiceError>); + #[get("/security_log/log_messages")] async fn get_log_messages( #[data] data: AppState, _: LoggedUser, query: Query, -) -> WarpResult { +) -> WarpResult { let query = query.into_inner(); let min_date: Option = query.min_date.map(Into::into); let max_date: Option = query.max_date.map(Into::into); @@ -378,24 +494,42 @@ async fn get_log_messages( ) .await .map_err(Into::::into)? + .map_ok(Into::into) .try_collect() .await .map_err(Into::::into)?; - Ok(rweb::reply::json(&messages)) + Ok(JsonBase::new(messages).into()) } +#[derive(RwebResponse)] +#[response(description = "Delete Log Messages", status = "NO_CONTENT")] +struct DeleteLogMessageResponse(HtmlBase); + #[delete("/security_log/log_messages/{id}")] async fn delete_log_message( #[data] data: AppState, _: LoggedUser, id: i32, -) -> WarpResult { +) -> WarpResult { let bytes = SystemdLogMessages::delete(&data.pool, id) .await .map_err(Into::::into)?; - Ok(rweb::reply::html(format_sstr!( - "deleted {id}, {bytes} modified" - ))) + Ok(HtmlBase::new(format_sstr!("deleted {id}, {bytes} modified")).into()) +} + +fn get_path(app: &AppState) -> BoxedFilter<(impl Reply,)> { + intrusion_attempts(app.clone()) + .or(map_script()) + .or(intrusion_attempts_all(app.clone())) + .or(intursion_log_get(app.clone())) + .or(intrusion_log_post(app.clone())) + .or(host_country_get(app.clone())) + .or(host_country_post(app.clone())) + .or(host_country_cleanup(app.clone())) + .or(user()) + .or(get_log_messages(app.clone())) + .or(delete_log_message(app.clone())) + .boxed() } async fn start_app() -> Result<(), AnyhowError> { @@ -423,17 +557,29 @@ async fn start_app() -> Result<(), AnyhowError> { .and_then(|s| s.parse().ok()) .unwrap_or(4086); - let intrusion_attempts_path = intrusion_attempts(app.clone()) - .or(map_script()) - .or(intrusion_attempts_all(app.clone())) - .or(intursion_log_get(app.clone())) - .or(intrusion_log_post(app.clone())) - .or(host_country_get(app.clone())) - .or(host_country_post(app.clone())) - .or(host_country_cleanup(app.clone())) - .or(user()) - .or(get_log_messages(app.clone())) - .or(delete_log_message(app.clone())); + let (spec, intrusion_attempts_path) = openapi::spec() + .info(Info { + title: "Frontend for AWS".into(), + description: "Web Frontend for AWS Services".into(), + version: env!("CARGO_PKG_VERSION").into(), + ..Info::default() + }) + .build(|| get_path(&app)); + let spec = Arc::new(spec); + let spec_json_path = rweb::path!("security_log" / "openapi" / "json") + .and(rweb::path::end()) + .map({ + let spec = spec.clone(); + move || rweb::reply::json(spec.as_ref()) + }); + + let spec_yaml = serde_yaml::to_string(spec.as_ref())?; + let spec_yaml_path = rweb::path!("security_log" / "openapi" / "yaml") + .and(rweb::path::end()) + .map(move || { + let reply = rweb::reply::html(spec_yaml.clone()); + rweb::reply::with_header(reply, CONTENT_TYPE, "text/yaml") + }); let cors = rweb::cors() .allow_methods(vec!["GET", "POST", "DELETE"]) @@ -441,7 +587,11 @@ async fn start_app() -> Result<(), AnyhowError> { .allow_any_origin() .build(); - let routes = intrusion_attempts_path.recover(error_response).with(cors); + let routes = intrusion_attempts_path + .or(spec_json_path) + .or(spec_yaml_path) + .recover(error_response) + .with(cors); let addr: SocketAddr = format_sstr!("127.0.0.1:{port}").parse()?; rweb::serve(routes).bind(addr).await; Ok(())