use axum::{
body::Bytes,
extract::State,
http::{HeaderMap, StatusCode},
response::IntoResponse,
routing::post,
Router,
};
use clap::Parser;
use freta::{
models::webhooks::{hmac_sha512, WebhookEvent, WebhookEventType, DIGEST_HEADER},
Client, Error, ImageId, Result, Secret,
};
use serde_json::Value;
use std::{io::stderr, net::SocketAddr, string::ToString};
use tracing::{error, info, level_filters::LevelFilter};
use tracing_subscriber::EnvFilter;
const API_ENDPOINT: &str = "/api/freta-analysis-webhook";
#[derive(Parser)]
struct Config {
#[arg(long, default_value_t = 3000, env = "FUNCTIONS_CUSTOMHANDLER_PORT")]
port: u16,
#[arg(long, env = "FRETA_HMAC_TOKEN")]
hmac_token: Option<Secret>,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env()
.map_err(|e| Error::Other("invalid env filter", e.to_string()))?,
)
.with_writer(stderr)
.init();
let config = Config::parse();
let app = Router::new()
.route(API_ENDPOINT, post(webhook_receiver))
.with_state(config.hmac_token);
let addr = SocketAddr::from(([0, 0, 0, 0], config.port));
info!("starting service on {}", addr);
let service = app.into_make_service();
axum::Server::bind(&addr)
.serve(service)
.await
.map_err(|e| Error::Other("service failed", format!("{e:?}")))?;
Ok(())
}
fn parse_and_validate(
bytes: &[u8],
hmac_header: Option<String>,
hmac_token: Option<Secret>,
) -> std::result::Result<WebhookEvent, Box<dyn std::error::Error>> {
let event: WebhookEvent = serde_json::from_slice(bytes)?;
if let Some(token) = hmac_token {
let Some(from_header) = hmac_header else {
return Err("hmac header is required".into());
};
let hmac = hmac_sha512(bytes, &token)?;
if !compare(&from_header, &hmac) {
return Err("hmac does not match".into());
}
}
Ok(event)
}
fn compare(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
let mut result = 0;
for (x, y) in a.bytes().zip(b.bytes()) {
result |= x ^ y;
}
result == 0
}
async fn show_kernel_banner_from_report(image_id: ImageId) -> Result<()> {
let client = Client::new().await?;
let report = client.artifacts_get(image_id, "report.json").await?;
let report_decoded: Value = serde_json::from_slice(&report)?;
let banner = report_decoded.get("info").and_then(|x| x.get("banner"));
info!("report: image_id:{image_id} banner:{banner:?}");
Ok(())
}
async fn webhook_receiver(
State(hmac_token): State<Option<Secret>>,
headers: HeaderMap,
body: Bytes,
) -> impl IntoResponse {
let hmac_header = headers
.get(DIGEST_HEADER)
.and_then(|h| h.to_str().map(ToString::to_string).ok());
let event = match parse_and_validate(&body, hmac_header, hmac_token) {
Ok(e) => e,
Err(err) => {
error!("unable to parse webhook payload: {err:?}");
return (StatusCode::BAD_REQUEST, "invalid payload");
}
};
info!("decoded {event:?}");
if event.event_type == WebhookEventType::ImageAnalysisCompleted {
if let Some(image_id) = event.image {
if let Err(err) = show_kernel_banner_from_report(image_id).await {
error!("unable to retrieve report from image: {err:?}");
}
}
}
(StatusCode::OK, "thanks")
}