style: Apply rustfmt with stable-only config
Some checks failed
CI Pipeline / Clippy Lints (push) Failing after 0s
CI Pipeline / Rust Unit Tests (push) Failing after 0s
CI Pipeline / Rust Format Check (push) Successful in 4s
CI Pipeline / Frontend Lint & Type Check (push) Failing after 0s
CI Pipeline / Security Audit (push) Failing after 3s
CI Pipeline / Build .deb & Release (push) Has been skipped
Some checks failed
CI Pipeline / Clippy Lints (push) Failing after 0s
CI Pipeline / Rust Unit Tests (push) Failing after 0s
CI Pipeline / Rust Format Check (push) Successful in 4s
CI Pipeline / Frontend Lint & Type Check (push) Failing after 0s
CI Pipeline / Security Audit (push) Failing after 3s
CI Pipeline / Build .deb & Release (push) Has been skipped
- Fixed rustfmt.toml to only use stable options (removed nightly-only) - Applied cargo fmt --all to fix formatting violations - Stable options: edition=2021, max_width=100, reorder_imports/modules, match_block_trailing_comma
This commit is contained in:
@ -4,10 +4,7 @@ use crate::{ReportParams, ReportType};
|
||||
use anyhow::Context;
|
||||
|
||||
/// Generate a CSV report and return the raw bytes.
|
||||
pub async fn generate_csv(
|
||||
pool: &sqlx::PgPool,
|
||||
params: &ReportParams,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
pub async fn generate_csv(pool: &sqlx::PgPool, params: &ReportParams) -> anyhow::Result<Vec<u8>> {
|
||||
match params.report_type {
|
||||
ReportType::Compliance => compliance_csv(pool, params).await,
|
||||
ReportType::PatchHistory => patch_history_csv(pool, params).await,
|
||||
@ -20,12 +17,10 @@ pub async fn generate_csv(
|
||||
// Compliance
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn compliance_csv(
|
||||
pool: &sqlx::PgPool,
|
||||
params: &ReportParams,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
async fn compliance_csv(pool: &sqlx::PgPool, params: &ReportParams) -> anyhow::Result<Vec<u8>> {
|
||||
let rows = if let Some(gid) = params.group_id {
|
||||
sqlx::query("
|
||||
sqlx::query(
|
||||
"
|
||||
SELECT
|
||||
h.id::text AS host_id,
|
||||
h.display_name,
|
||||
@ -47,13 +42,15 @@ WHERE h.id IN (
|
||||
)
|
||||
GROUP BY h.id, pd.total_packages, pd.pending_patches
|
||||
ORDER BY compliance_pct ASC
|
||||
")
|
||||
",
|
||||
)
|
||||
.bind(gid)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.context("compliance query (group filter) failed")?
|
||||
} else {
|
||||
sqlx::query("
|
||||
sqlx::query(
|
||||
"
|
||||
SELECT
|
||||
h.id::text AS host_id,
|
||||
h.display_name,
|
||||
@ -72,7 +69,8 @@ LEFT JOIN host_groups hg ON hg.host_id = h.id
|
||||
LEFT JOIN groups g ON g.id = hg.group_id
|
||||
GROUP BY h.id, pd.total_packages, pd.pending_patches
|
||||
ORDER BY compliance_pct ASC
|
||||
")
|
||||
",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.context("compliance query failed")?
|
||||
@ -80,23 +78,29 @@ ORDER BY compliance_pct ASC
|
||||
|
||||
let mut wtr = csv::Writer::from_writer(vec![]);
|
||||
wtr.write_record(&[
|
||||
"host_id", "display_name", "fqdn", "group_names",
|
||||
"total_packages", "pending_patches", "compliance_pct",
|
||||
"last_patch_at", "health_status",
|
||||
"host_id",
|
||||
"display_name",
|
||||
"fqdn",
|
||||
"group_names",
|
||||
"total_packages",
|
||||
"pending_patches",
|
||||
"compliance_pct",
|
||||
"last_patch_at",
|
||||
"health_status",
|
||||
])?;
|
||||
|
||||
for row in &rows {
|
||||
use sqlx::Row;
|
||||
let host_id: String = row.try_get("host_id").unwrap_or_default();
|
||||
let display_name: String = row.try_get("display_name").unwrap_or_default();
|
||||
let fqdn: String = row.try_get("fqdn").unwrap_or_default();
|
||||
let group_names: String = row.try_get("group_names").unwrap_or_default();
|
||||
let total_packages: i64 = row.try_get("total_packages").unwrap_or(0);
|
||||
let pending_patches: i64 = row.try_get("pending_patches").unwrap_or(0);
|
||||
let compliance_pct: f64 = row.try_get("compliance_pct").unwrap_or(0.0);
|
||||
let host_id: String = row.try_get("host_id").unwrap_or_default();
|
||||
let display_name: String = row.try_get("display_name").unwrap_or_default();
|
||||
let fqdn: String = row.try_get("fqdn").unwrap_or_default();
|
||||
let group_names: String = row.try_get("group_names").unwrap_or_default();
|
||||
let total_packages: i64 = row.try_get("total_packages").unwrap_or(0);
|
||||
let pending_patches: i64 = row.try_get("pending_patches").unwrap_or(0);
|
||||
let compliance_pct: f64 = row.try_get("compliance_pct").unwrap_or(0.0);
|
||||
let last_patch_at: Option<chrono::DateTime<chrono::Utc>> =
|
||||
row.try_get("last_patch_at").unwrap_or(None);
|
||||
let health_status: String = row.try_get("health_status").unwrap_or_default();
|
||||
let health_status: String = row.try_get("health_status").unwrap_or_default();
|
||||
|
||||
wtr.write_record(&[
|
||||
host_id,
|
||||
@ -118,11 +122,9 @@ ORDER BY compliance_pct ASC
|
||||
// Patch history
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn patch_history_csv(
|
||||
pool: &sqlx::PgPool,
|
||||
params: &ReportParams,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
let rows = sqlx::query("
|
||||
async fn patch_history_csv(pool: &sqlx::PgPool, params: &ReportParams) -> anyhow::Result<Vec<u8>> {
|
||||
let rows = sqlx::query(
|
||||
"
|
||||
SELECT
|
||||
pj.id::text AS job_id,
|
||||
pj.kind::text AS job_kind,
|
||||
@ -141,7 +143,8 @@ LEFT JOIN users u ON u.id = pj.created_by_user_id
|
||||
WHERE ($1::timestamptz IS NULL OR pjh.started_at >= $1)
|
||||
AND ($2::timestamptz IS NULL OR pjh.started_at <= $2)
|
||||
ORDER BY pjh.started_at DESC
|
||||
")
|
||||
",
|
||||
)
|
||||
.bind(params.from)
|
||||
.bind(params.to)
|
||||
.fetch_all(pool)
|
||||
@ -150,24 +153,32 @@ ORDER BY pjh.started_at DESC
|
||||
|
||||
let mut wtr = csv::Writer::from_writer(vec![]);
|
||||
wtr.write_record(&[
|
||||
"job_id", "job_kind", "job_status", "host_display_name", "host_fqdn",
|
||||
"package_count", "started_at", "completed_at", "duration_seconds", "operator",
|
||||
"job_id",
|
||||
"job_kind",
|
||||
"job_status",
|
||||
"host_display_name",
|
||||
"host_fqdn",
|
||||
"package_count",
|
||||
"started_at",
|
||||
"completed_at",
|
||||
"duration_seconds",
|
||||
"operator",
|
||||
])?;
|
||||
|
||||
for row in &rows {
|
||||
use sqlx::Row;
|
||||
let job_id: String = row.try_get("job_id").unwrap_or_default();
|
||||
let job_kind: String = row.try_get("job_kind").unwrap_or_default();
|
||||
let job_status: String = row.try_get("job_status").unwrap_or_default();
|
||||
let job_id: String = row.try_get("job_id").unwrap_or_default();
|
||||
let job_kind: String = row.try_get("job_kind").unwrap_or_default();
|
||||
let job_status: String = row.try_get("job_status").unwrap_or_default();
|
||||
let display_name: String = row.try_get("display_name").unwrap_or_default();
|
||||
let fqdn: String = row.try_get("fqdn").unwrap_or_default();
|
||||
let package_count: i64 = row.try_get("package_count").unwrap_or(0);
|
||||
let fqdn: String = row.try_get("fqdn").unwrap_or_default();
|
||||
let package_count: i64 = row.try_get("package_count").unwrap_or(0);
|
||||
let started_at: Option<chrono::DateTime<chrono::Utc>> =
|
||||
row.try_get("started_at").unwrap_or(None);
|
||||
let completed_at: Option<chrono::DateTime<chrono::Utc>> =
|
||||
row.try_get("completed_at").unwrap_or(None);
|
||||
let duration_seconds: Option<i64> = row.try_get("duration_seconds").unwrap_or(None);
|
||||
let operator: String = row.try_get("operator").unwrap_or_default();
|
||||
let operator: String = row.try_get("operator").unwrap_or_default();
|
||||
|
||||
wtr.write_record(&[
|
||||
job_id,
|
||||
@ -190,17 +201,21 @@ ORDER BY pjh.started_at DESC
|
||||
// Vulnerability
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn vulnerability_csv(
|
||||
pool: &sqlx::PgPool,
|
||||
params: &ReportParams,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
async fn vulnerability_csv(pool: &sqlx::PgPool, params: &ReportParams) -> anyhow::Result<Vec<u8>> {
|
||||
let mut wtr = csv::Writer::from_writer(vec![]);
|
||||
wtr.write_record(&[
|
||||
"host_id", "display_name", "fqdn", "cve_id",
|
||||
"package_name", "severity", "available_version", "last_seen_at",
|
||||
"host_id",
|
||||
"display_name",
|
||||
"fqdn",
|
||||
"cve_id",
|
||||
"package_name",
|
||||
"severity",
|
||||
"available_version",
|
||||
"last_seen_at",
|
||||
])?;
|
||||
|
||||
let result = sqlx::query("
|
||||
let result = sqlx::query(
|
||||
"
|
||||
SELECT
|
||||
h.id::text AS host_id,
|
||||
h.display_name,
|
||||
@ -224,7 +239,8 @@ ORDER BY
|
||||
ELSE 4
|
||||
END,
|
||||
h.display_name
|
||||
")
|
||||
",
|
||||
)
|
||||
.bind(params.from)
|
||||
.bind(params.to)
|
||||
.fetch_all(pool)
|
||||
@ -234,12 +250,12 @@ ORDER BY
|
||||
Ok(rows) => {
|
||||
for row in &rows {
|
||||
use sqlx::Row;
|
||||
let host_id: String = row.try_get("host_id").unwrap_or_default();
|
||||
let display_name: String = row.try_get("display_name").unwrap_or_default();
|
||||
let fqdn: String = row.try_get("fqdn").unwrap_or_default();
|
||||
let cve_id: String = row.try_get("cve_id").unwrap_or_default();
|
||||
let package_name: String = row.try_get("package_name").unwrap_or_default();
|
||||
let severity: String = row.try_get("severity").unwrap_or_default();
|
||||
let host_id: String = row.try_get("host_id").unwrap_or_default();
|
||||
let display_name: String = row.try_get("display_name").unwrap_or_default();
|
||||
let fqdn: String = row.try_get("fqdn").unwrap_or_default();
|
||||
let cve_id: String = row.try_get("cve_id").unwrap_or_default();
|
||||
let package_name: String = row.try_get("package_name").unwrap_or_default();
|
||||
let severity: String = row.try_get("severity").unwrap_or_default();
|
||||
let available_version: String =
|
||||
row.try_get("available_version").unwrap_or_default();
|
||||
let last_seen_at: Option<chrono::DateTime<chrono::Utc>> =
|
||||
@ -256,15 +272,21 @@ ORDER BY
|
||||
last_seen_at.map(|d| d.to_rfc3339()).unwrap_or_default(),
|
||||
])?;
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "vulnerability query failed — returning empty rows");
|
||||
// write a comment row indicating empty data
|
||||
wtr.write_record(&[
|
||||
"(no data)", "", "", "", "", "", "",
|
||||
"(no data)",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
&format!("query error: {}", e),
|
||||
])?;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(wtr.into_inner().context("csv flush failed")?)
|
||||
@ -274,11 +296,9 @@ ORDER BY
|
||||
// Audit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn audit_csv(
|
||||
pool: &sqlx::PgPool,
|
||||
params: &ReportParams,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
let rows = sqlx::query("
|
||||
async fn audit_csv(pool: &sqlx::PgPool, params: &ReportParams) -> anyhow::Result<Vec<u8>> {
|
||||
let rows = sqlx::query(
|
||||
"
|
||||
SELECT
|
||||
id::text AS id,
|
||||
created_at,
|
||||
@ -293,7 +313,8 @@ WHERE ($1::timestamptz IS NULL OR created_at >= $1)
|
||||
AND ($2::timestamptz IS NULL OR created_at <= $2)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10000
|
||||
")
|
||||
",
|
||||
)
|
||||
.bind(params.from)
|
||||
.bind(params.to)
|
||||
.fetch_all(pool)
|
||||
@ -302,21 +323,27 @@ LIMIT 10000
|
||||
|
||||
let mut wtr = csv::Writer::from_writer(vec![]);
|
||||
wtr.write_record(&[
|
||||
"id", "created_at", "action", "actor_username",
|
||||
"target_type", "target_id", "ip_address", "request_id",
|
||||
"id",
|
||||
"created_at",
|
||||
"action",
|
||||
"actor_username",
|
||||
"target_type",
|
||||
"target_id",
|
||||
"ip_address",
|
||||
"request_id",
|
||||
])?;
|
||||
|
||||
for row in &rows {
|
||||
use sqlx::Row;
|
||||
let id: String = row.try_get("id").unwrap_or_default();
|
||||
let id: String = row.try_get("id").unwrap_or_default();
|
||||
let created_at: Option<chrono::DateTime<chrono::Utc>> =
|
||||
row.try_get("created_at").unwrap_or(None);
|
||||
let action: String = row.try_get("action").unwrap_or_default();
|
||||
let action: String = row.try_get("action").unwrap_or_default();
|
||||
let actor_username: String = row.try_get("actor_username").unwrap_or_default();
|
||||
let target_type: String = row.try_get("target_type").unwrap_or_default();
|
||||
let target_id: String = row.try_get("target_id").unwrap_or_default();
|
||||
let ip_address: String = row.try_get("ip_address").unwrap_or_default();
|
||||
let request_id: String = row.try_get("request_id").unwrap_or_default();
|
||||
let target_type: String = row.try_get("target_type").unwrap_or_default();
|
||||
let target_id: String = row.try_get("target_id").unwrap_or_default();
|
||||
let ip_address: String = row.try_get("ip_address").unwrap_or_default();
|
||||
let request_id: String = row.try_get("request_id").unwrap_or_default();
|
||||
|
||||
wtr.write_record(&[
|
||||
id,
|
||||
|
||||
@ -6,9 +6,8 @@ use crate::{ReportParams, ReportType};
|
||||
use anyhow::Context;
|
||||
use plotters::prelude::*;
|
||||
use printpdf::{
|
||||
BuiltinFont, ColorBits, ColorSpace, Image, ImageTransform, ImageXObject,
|
||||
IndirectFontRef, Mm, PdfDocument, PdfLayerIndex, PdfLayerReference,
|
||||
PdfPageIndex, Px,
|
||||
BuiltinFont, ColorBits, ColorSpace, Image, ImageTransform, ImageXObject, IndirectFontRef, Mm,
|
||||
PdfDocument, PdfLayerIndex, PdfLayerReference, PdfPageIndex, Px,
|
||||
};
|
||||
|
||||
const PAGE_W: f32 = 297.0; // A4 landscape width (mm)
|
||||
@ -23,15 +22,12 @@ const NEW_PAGE_THRESHOLD: f32 = 20.0;
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Generate a PDF report and return the raw bytes.
|
||||
pub async fn generate_pdf(
|
||||
pool: &sqlx::PgPool,
|
||||
params: &ReportParams,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
pub async fn generate_pdf(pool: &sqlx::PgPool, params: &ReportParams) -> anyhow::Result<Vec<u8>> {
|
||||
match params.report_type {
|
||||
ReportType::Compliance => compliance_pdf(pool, params).await,
|
||||
ReportType::PatchHistory => patch_history_pdf(pool, params).await,
|
||||
ReportType::Compliance => compliance_pdf(pool, params).await,
|
||||
ReportType::PatchHistory => patch_history_pdf(pool, params).await,
|
||||
ReportType::Vulnerability => vulnerability_pdf(pool, params).await,
|
||||
ReportType::Audit => audit_pdf(pool, params).await,
|
||||
ReportType::Audit => audit_pdf(pool, params).await,
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,15 +47,10 @@ fn render_bar_chart(
|
||||
let mut pixel_buf = vec![0u8; (W * H * 3) as usize];
|
||||
|
||||
{
|
||||
let root = BitMapBackend::with_buffer(&mut pixel_buf, (W, H))
|
||||
.into_drawing_area();
|
||||
let root = BitMapBackend::with_buffer(&mut pixel_buf, (W, H)).into_drawing_area();
|
||||
root.fill(&WHITE)?;
|
||||
|
||||
let max_val = values
|
||||
.iter()
|
||||
.cloned()
|
||||
.fold(0.0_f64, f64::max)
|
||||
.max(1.0);
|
||||
let max_val = values.iter().cloned().fold(0.0_f64, f64::max).max(1.0);
|
||||
let n = labels.len().max(1);
|
||||
|
||||
let mut chart = ChartBuilder::on(&root)
|
||||
@ -76,7 +67,11 @@ fn render_bar_chart(
|
||||
labels
|
||||
.get(*idx)
|
||||
.map(|s| {
|
||||
if s.len() > 12 { s[..12].to_string() } else { s.clone() }
|
||||
if s.len() > 12 {
|
||||
s[..12].to_string()
|
||||
} else {
|
||||
s.clone()
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
@ -119,7 +114,7 @@ impl PdfBuilder {
|
||||
fn new(title: &str) -> anyhow::Result<Self> {
|
||||
let doc = PdfDocument::empty(title);
|
||||
let (page_idx, layer_idx) = doc.add_page(Mm(PAGE_W), Mm(PAGE_H), "Layer 1");
|
||||
let font = doc.add_builtin_font(BuiltinFont::Helvetica)?;
|
||||
let font = doc.add_builtin_font(BuiltinFont::Helvetica)?;
|
||||
let font_bold = doc.add_builtin_font(BuiltinFont::HelveticaBold)?;
|
||||
Ok(Self {
|
||||
doc,
|
||||
@ -212,19 +207,31 @@ impl PdfBuilder {
|
||||
fn write_title_page(pdf: &mut PdfBuilder, title: &str, params: &ReportParams) {
|
||||
pdf.write_text(title, 24.0, MARGIN, 160.0, true);
|
||||
pdf.write_text(
|
||||
&format!("Generated: {}", chrono::Utc::now().format("%Y-%m-%d %H:%M UTC")),
|
||||
11.0, MARGIN, 148.0, false,
|
||||
&format!(
|
||||
"Generated: {}",
|
||||
chrono::Utc::now().format("%Y-%m-%d %H:%M UTC")
|
||||
),
|
||||
11.0,
|
||||
MARGIN,
|
||||
148.0,
|
||||
false,
|
||||
);
|
||||
if let Some(from) = params.from {
|
||||
pdf.write_text(
|
||||
&format!("From: {}", from.format("%Y-%m-%d")),
|
||||
10.0, MARGIN, 140.0, false,
|
||||
10.0,
|
||||
MARGIN,
|
||||
140.0,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if let Some(to) = params.to {
|
||||
pdf.write_text(
|
||||
&format!("To: {}", to.format("%Y-%m-%d")),
|
||||
10.0, MARGIN, 134.0, false,
|
||||
10.0,
|
||||
MARGIN,
|
||||
134.0,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if let Some(gid) = params.group_id {
|
||||
@ -237,13 +244,11 @@ fn write_title_page(pdf: &mut PdfBuilder, title: &str, params: &ReportParams) {
|
||||
// Compliance PDF
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn compliance_pdf(
|
||||
pool: &sqlx::PgPool,
|
||||
params: &ReportParams,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
async fn compliance_pdf(pool: &sqlx::PgPool, params: &ReportParams) -> anyhow::Result<Vec<u8>> {
|
||||
use sqlx::Row;
|
||||
let rows = if let Some(gid) = params.group_id {
|
||||
sqlx::query("
|
||||
sqlx::query(
|
||||
"
|
||||
SELECT h.display_name, h.fqdn,
|
||||
COALESCE(pd.total_packages,0) AS total_packages,
|
||||
COALESCE(pd.pending_patches,0) AS pending_patches,
|
||||
@ -254,11 +259,15 @@ SELECT h.display_name, h.fqdn,
|
||||
FROM hosts h LEFT JOIN host_patch_data pd ON pd.host_id=h.id
|
||||
WHERE h.id IN (SELECT host_id FROM host_groups WHERE group_id=$1)
|
||||
GROUP BY h.id,pd.total_packages,pd.pending_patches
|
||||
ORDER BY compliance_pct ASC")
|
||||
.bind(gid).fetch_all(pool).await
|
||||
.context("compliance PDF query (group) failed")?
|
||||
ORDER BY compliance_pct ASC",
|
||||
)
|
||||
.bind(gid)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.context("compliance PDF query (group) failed")?
|
||||
} else {
|
||||
sqlx::query("
|
||||
sqlx::query(
|
||||
"
|
||||
SELECT h.display_name, h.fqdn,
|
||||
COALESCE(pd.total_packages,0) AS total_packages,
|
||||
COALESCE(pd.pending_patches,0) AS pending_patches,
|
||||
@ -268,26 +277,55 @@ SELECT h.display_name, h.fqdn,
|
||||
h.health_status::text AS health_status
|
||||
FROM hosts h LEFT JOIN host_patch_data pd ON pd.host_id=h.id
|
||||
GROUP BY h.id,pd.total_packages,pd.pending_patches
|
||||
ORDER BY compliance_pct ASC")
|
||||
.fetch_all(pool).await
|
||||
.context("compliance PDF query failed")?
|
||||
ORDER BY compliance_pct ASC",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.context("compliance PDF query failed")?
|
||||
};
|
||||
let labels: Vec<String> = rows.iter().map(|r| r.try_get::<String,_>("display_name").unwrap_or_default()).collect();
|
||||
let values: Vec<f64> = rows.iter().map(|r| r.try_get::<f64,_>("compliance_pct").unwrap_or(0.0)).collect();
|
||||
let labels: Vec<String> = rows
|
||||
.iter()
|
||||
.map(|r| r.try_get::<String, _>("display_name").unwrap_or_default())
|
||||
.collect();
|
||||
let values: Vec<f64> = rows
|
||||
.iter()
|
||||
.map(|r| r.try_get::<f64, _>("compliance_pct").unwrap_or(0.0))
|
||||
.collect();
|
||||
let mut pdf = PdfBuilder::new("Compliance Report")?;
|
||||
write_title_page(&mut pdf, "Compliance Report", params);
|
||||
let col_x: &[f32] = &[MARGIN, 65.0, 130.0, 165.0, 200.0, 235.0];
|
||||
pdf.table_row(&["Host","FQDN","Total Pkgs","Pending","Compliance %","Status"], col_x, 9.0, true);
|
||||
pdf.table_row(
|
||||
&[
|
||||
"Host",
|
||||
"FQDN",
|
||||
"Total Pkgs",
|
||||
"Pending",
|
||||
"Compliance %",
|
||||
"Status",
|
||||
],
|
||||
col_x,
|
||||
9.0,
|
||||
true,
|
||||
);
|
||||
for row in &rows {
|
||||
let name: String = row.try_get("display_name").unwrap_or_default();
|
||||
let fqdn: String = row.try_get("fqdn").unwrap_or_default();
|
||||
let total: i64 = row.try_get("total_packages").unwrap_or(0);
|
||||
let pend: i64 = row.try_get("pending_patches").unwrap_or(0);
|
||||
let pct: f64 = row.try_get("compliance_pct").unwrap_or(0.0);
|
||||
let name: String = row.try_get("display_name").unwrap_or_default();
|
||||
let fqdn: String = row.try_get("fqdn").unwrap_or_default();
|
||||
let total: i64 = row.try_get("total_packages").unwrap_or(0);
|
||||
let pend: i64 = row.try_get("pending_patches").unwrap_or(0);
|
||||
let pct: f64 = row.try_get("compliance_pct").unwrap_or(0.0);
|
||||
let status: String = row.try_get("health_status").unwrap_or_default();
|
||||
pdf.table_row(
|
||||
&[&name,&fqdn,&total.to_string(),&pend.to_string(),&format!("{:.1}%",pct),&status],
|
||||
col_x, 8.0, false,
|
||||
&[
|
||||
&name,
|
||||
&fqdn,
|
||||
&total.to_string(),
|
||||
&pend.to_string(),
|
||||
&format!("{:.1}%", pct),
|
||||
&status,
|
||||
],
|
||||
col_x,
|
||||
8.0,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if !labels.is_empty() {
|
||||
@ -298,7 +336,7 @@ ORDER BY compliance_pct ASC")
|
||||
if let Err(e) = pdf.embed_image(raw, w, h, MARGIN, 10.0, 0.18, 0.18) {
|
||||
tracing::warn!(error = %e, "chart embed failed");
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => tracing::warn!(error = %e, "chart render failed"),
|
||||
}
|
||||
}
|
||||
@ -309,12 +347,10 @@ ORDER BY compliance_pct ASC")
|
||||
// Patch history PDF
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn patch_history_pdf(
|
||||
pool: &sqlx::PgPool,
|
||||
params: &ReportParams,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
async fn patch_history_pdf(pool: &sqlx::PgPool, params: &ReportParams) -> anyhow::Result<Vec<u8>> {
|
||||
use sqlx::Row;
|
||||
let rows = sqlx::query("
|
||||
let rows = sqlx::query(
|
||||
"
|
||||
SELECT pj.kind::text AS job_kind, pj.status::text AS job_status,
|
||||
h.display_name, h.fqdn, pjh.started_at, pjh.completed_at,
|
||||
EXTRACT(EPOCH FROM (pjh.completed_at-pjh.started_at))::bigint AS duration_seconds,
|
||||
@ -325,33 +361,71 @@ JOIN hosts h ON h.id=pjh.host_id
|
||||
LEFT JOIN users u ON u.id=pj.created_by_user_id
|
||||
WHERE ($1::timestamptz IS NULL OR pjh.started_at>=$1)
|
||||
AND ($2::timestamptz IS NULL OR pjh.started_at<=$2)
|
||||
ORDER BY pjh.started_at DESC")
|
||||
.bind(params.from).bind(params.to).fetch_all(pool).await
|
||||
.context("patch history PDF query failed")?;
|
||||
let mut dc: std::collections::BTreeMap<String,f64> = std::collections::BTreeMap::new();
|
||||
ORDER BY pjh.started_at DESC",
|
||||
)
|
||||
.bind(params.from)
|
||||
.bind(params.to)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.context("patch history PDF query failed")?;
|
||||
let mut dc: std::collections::BTreeMap<String, f64> = std::collections::BTreeMap::new();
|
||||
for row in &rows {
|
||||
if let Ok(Some(s)) = row.try_get::<Option<chrono::DateTime<chrono::Utc>>,_>("started_at") {
|
||||
if let Ok(Some(s)) = row.try_get::<Option<chrono::DateTime<chrono::Utc>>, _>("started_at") {
|
||||
*dc.entry(s.format("%Y-%m-%d").to_string()).or_insert(0.0) += 1.0;
|
||||
}
|
||||
}
|
||||
let cl: Vec<String> = dc.keys().cloned().collect();
|
||||
let cv: Vec<f64> = dc.values().cloned().collect();
|
||||
let cv: Vec<f64> = dc.values().cloned().collect();
|
||||
let mut pdf = PdfBuilder::new("Patch History Report")?;
|
||||
write_title_page(&mut pdf, "Patch History Report", params);
|
||||
let col_x: &[f32] = &[MARGIN,45.0,80.0,115.0,155.0,200.0,245.0,270.0];
|
||||
pdf.table_row(&["Kind","Status","Host","FQDN","Started","Completed","Dur(s)","Operator"], col_x, 9.0, true);
|
||||
let col_x: &[f32] = &[MARGIN, 45.0, 80.0, 115.0, 155.0, 200.0, 245.0, 270.0];
|
||||
pdf.table_row(
|
||||
&[
|
||||
"Kind",
|
||||
"Status",
|
||||
"Host",
|
||||
"FQDN",
|
||||
"Started",
|
||||
"Completed",
|
||||
"Dur(s)",
|
||||
"Operator",
|
||||
],
|
||||
col_x,
|
||||
9.0,
|
||||
true,
|
||||
);
|
||||
for row in &rows {
|
||||
let kind: String = row.try_get("job_kind").unwrap_or_default();
|
||||
let kind: String = row.try_get("job_kind").unwrap_or_default();
|
||||
let status: String = row.try_get("job_status").unwrap_or_default();
|
||||
let name: String = row.try_get("display_name").unwrap_or_default();
|
||||
let fqdn: String = row.try_get("fqdn").unwrap_or_default();
|
||||
let started: String = row.try_get::<Option<chrono::DateTime<chrono::Utc>>,_>("started_at")
|
||||
.unwrap_or(None).map(|d| d.format("%Y-%m-%d %H:%M").to_string()).unwrap_or_default();
|
||||
let completed: String = row.try_get::<Option<chrono::DateTime<chrono::Utc>>,_>("completed_at")
|
||||
.unwrap_or(None).map(|d| d.format("%Y-%m-%d %H:%M").to_string()).unwrap_or_default();
|
||||
let dur: i64 = row.try_get("duration_seconds").unwrap_or(0);
|
||||
let op: String = row.try_get("operator").unwrap_or_default();
|
||||
pdf.table_row(&[&kind,&status,&name,&fqdn,&started,&completed,&dur.to_string(),&op], col_x, 8.0, false);
|
||||
let name: String = row.try_get("display_name").unwrap_or_default();
|
||||
let fqdn: String = row.try_get("fqdn").unwrap_or_default();
|
||||
let started: String = row
|
||||
.try_get::<Option<chrono::DateTime<chrono::Utc>>, _>("started_at")
|
||||
.unwrap_or(None)
|
||||
.map(|d| d.format("%Y-%m-%d %H:%M").to_string())
|
||||
.unwrap_or_default();
|
||||
let completed: String = row
|
||||
.try_get::<Option<chrono::DateTime<chrono::Utc>>, _>("completed_at")
|
||||
.unwrap_or(None)
|
||||
.map(|d| d.format("%Y-%m-%d %H:%M").to_string())
|
||||
.unwrap_or_default();
|
||||
let dur: i64 = row.try_get("duration_seconds").unwrap_or(0);
|
||||
let op: String = row.try_get("operator").unwrap_or_default();
|
||||
pdf.table_row(
|
||||
&[
|
||||
&kind,
|
||||
&status,
|
||||
&name,
|
||||
&fqdn,
|
||||
&started,
|
||||
&completed,
|
||||
&dur.to_string(),
|
||||
&op,
|
||||
],
|
||||
col_x,
|
||||
8.0,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if !cl.is_empty() {
|
||||
match render_bar_chart(&cl, &cv, "Jobs per Day") {
|
||||
@ -361,7 +435,7 @@ ORDER BY pjh.started_at DESC")
|
||||
if let Err(e) = pdf.embed_image(raw, w, h, MARGIN, 10.0, 0.18, 0.18) {
|
||||
tracing::warn!(error = %e, "chart embed failed");
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => tracing::warn!(error = %e, "chart render failed"),
|
||||
}
|
||||
}
|
||||
@ -372,10 +446,7 @@ ORDER BY pjh.started_at DESC")
|
||||
// Vulnerability PDF
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn vulnerability_pdf(
|
||||
pool: &sqlx::PgPool,
|
||||
params: &ReportParams,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
async fn vulnerability_pdf(pool: &sqlx::PgPool, params: &ReportParams) -> anyhow::Result<Vec<u8>> {
|
||||
use sqlx::Row;
|
||||
// Query DB FIRST (before creating any non-Send PdfBuilder)
|
||||
let query_result = sqlx::query("
|
||||
@ -393,61 +464,104 @@ ORDER BY CASE cve.severity WHEN 'critical' THEN 1 WHEN 'high' THEN 2 WHEN 'mediu
|
||||
// Now create PdfBuilder (non-Send Rc types) after all awaits
|
||||
let mut pdf = PdfBuilder::new("Vulnerability Report")?;
|
||||
write_title_page(&mut pdf, "Vulnerability Exposure Report", params);
|
||||
let col_x: &[f32] = &[MARGIN,55.0,100.0,130.0,175.0,215.0,255.0];
|
||||
pdf.table_row(&["Host","FQDN","CVE ID","Package","Severity","Fix Version","Last Seen"], col_x, 9.0, true);
|
||||
let col_x: &[f32] = &[MARGIN, 55.0, 100.0, 130.0, 175.0, 215.0, 255.0];
|
||||
pdf.table_row(
|
||||
&[
|
||||
"Host",
|
||||
"FQDN",
|
||||
"CVE ID",
|
||||
"Package",
|
||||
"Severity",
|
||||
"Fix Version",
|
||||
"Last Seen",
|
||||
],
|
||||
col_x,
|
||||
9.0,
|
||||
true,
|
||||
);
|
||||
match query_result {
|
||||
Ok(rows) => {
|
||||
for row in &rows {
|
||||
let name: String = row.try_get("display_name").unwrap_or_default();
|
||||
let fqdn: String = row.try_get("fqdn").unwrap_or_default();
|
||||
let cve: String = row.try_get("cve_id").unwrap_or_default();
|
||||
let pkg: String = row.try_get("package_name").unwrap_or_default();
|
||||
let sev: String = row.try_get("severity").unwrap_or_default();
|
||||
let fix: String = row.try_get("available_version").unwrap_or_default();
|
||||
let seen: String = row.try_get::<Option<chrono::DateTime<chrono::Utc>>,_>("last_seen_at")
|
||||
.unwrap_or(None).map(|d| d.format("%Y-%m-%d").to_string()).unwrap_or_default();
|
||||
pdf.table_row(&[&name,&fqdn,&cve,&pkg,&sev,&fix,&seen], col_x, 8.0, false);
|
||||
let cve: String = row.try_get("cve_id").unwrap_or_default();
|
||||
let pkg: String = row.try_get("package_name").unwrap_or_default();
|
||||
let sev: String = row.try_get("severity").unwrap_or_default();
|
||||
let fix: String = row.try_get("available_version").unwrap_or_default();
|
||||
let seen: String = row
|
||||
.try_get::<Option<chrono::DateTime<chrono::Utc>>, _>("last_seen_at")
|
||||
.unwrap_or(None)
|
||||
.map(|d| d.format("%Y-%m-%d").to_string())
|
||||
.unwrap_or_default();
|
||||
pdf.table_row(
|
||||
&[&name, &fqdn, &cve, &pkg, &sev, &fix, &seen],
|
||||
col_x,
|
||||
8.0,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "vulnerability PDF query failed");
|
||||
let y = pdf.current_y;
|
||||
pdf.write_text(&format!("No data: {}", e), 10.0, MARGIN, y, false);
|
||||
}
|
||||
},
|
||||
}
|
||||
pdf.save()
|
||||
}
|
||||
|
||||
|
||||
async fn audit_pdf(
|
||||
pool: &sqlx::PgPool,
|
||||
params: &ReportParams,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
async fn audit_pdf(pool: &sqlx::PgPool, params: &ReportParams) -> anyhow::Result<Vec<u8>> {
|
||||
use sqlx::Row;
|
||||
let rows = sqlx::query("
|
||||
let rows = sqlx::query(
|
||||
"
|
||||
SELECT id::text AS id, created_at, action::text AS action,
|
||||
actor_username, target_type, target_id,
|
||||
ip_address::text AS ip_address, request_id
|
||||
FROM audit_log
|
||||
WHERE ($1::timestamptz IS NULL OR created_at>=$1)
|
||||
AND ($2::timestamptz IS NULL OR created_at<=$2)
|
||||
ORDER BY created_at DESC LIMIT 10000")
|
||||
.bind(params.from).bind(params.to).fetch_all(pool).await
|
||||
.context("audit PDF query failed")?;
|
||||
ORDER BY created_at DESC LIMIT 10000",
|
||||
)
|
||||
.bind(params.from)
|
||||
.bind(params.to)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.context("audit PDF query failed")?;
|
||||
let mut pdf = PdfBuilder::new("Audit Trail Report")?;
|
||||
write_title_page(&mut pdf, "Audit Trail Report", params);
|
||||
let col_x: &[f32] = &[MARGIN,50.0,95.0,135.0,175.0,215.0,255.0];
|
||||
pdf.table_row(&["Timestamp","Action","Actor","Target Type","Target ID","IP","Request ID"], col_x, 9.0, true);
|
||||
let col_x: &[f32] = &[MARGIN, 50.0, 95.0, 135.0, 175.0, 215.0, 255.0];
|
||||
pdf.table_row(
|
||||
&[
|
||||
"Timestamp",
|
||||
"Action",
|
||||
"Actor",
|
||||
"Target Type",
|
||||
"Target ID",
|
||||
"IP",
|
||||
"Request ID",
|
||||
],
|
||||
col_x,
|
||||
9.0,
|
||||
true,
|
||||
);
|
||||
for row in &rows {
|
||||
let created: String = row.try_get::<Option<chrono::DateTime<chrono::Utc>>,_>("created_at")
|
||||
.unwrap_or(None).map(|d| d.format("%Y-%m-%d %H:%M").to_string()).unwrap_or_default();
|
||||
let created: String = row
|
||||
.try_get::<Option<chrono::DateTime<chrono::Utc>>, _>("created_at")
|
||||
.unwrap_or(None)
|
||||
.map(|d| d.format("%Y-%m-%d %H:%M").to_string())
|
||||
.unwrap_or_default();
|
||||
let action: String = row.try_get("action").unwrap_or_default();
|
||||
let actor: String = row.try_get("actor_username").unwrap_or_default();
|
||||
let ttype: String = row.try_get("target_type").unwrap_or_default();
|
||||
let tid: String = row.try_get("target_id").unwrap_or_default();
|
||||
let ip: String = row.try_get("ip_address").unwrap_or_default();
|
||||
let req: String = row.try_get("request_id").unwrap_or_default();
|
||||
pdf.table_row(&[&created,&action,&actor,&ttype,&tid,&ip,&req], col_x, 8.0, false);
|
||||
let actor: String = row.try_get("actor_username").unwrap_or_default();
|
||||
let ttype: String = row.try_get("target_type").unwrap_or_default();
|
||||
let tid: String = row.try_get("target_id").unwrap_or_default();
|
||||
let ip: String = row.try_get("ip_address").unwrap_or_default();
|
||||
let req: String = row.try_get("request_id").unwrap_or_default();
|
||||
pdf.table_row(
|
||||
&[&created, &action, &actor, &ttype, &tid, &ip, &req],
|
||||
col_x,
|
||||
8.0,
|
||||
false,
|
||||
);
|
||||
}
|
||||
pdf.save()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user