Revert "ci: adapt CI to ubuntu-22.04 runner with proven linux_patch_api patterns"
This reverts commit f8bac85903.
This commit is contained in:
@ -564,214 +564,4 @@ ORDER BY created_at DESC LIMIT 10000",
|
||||
);
|
||||
}
|
||||
pdf.save()
|
||||
}
|
||||
if let Some(gid) = params.group_id {
|
||||
pdf.write_text(
|
||||
&format!("Group: {}", gid),
|
||||
10.0, MARGIN, 128.0, false,
|
||||
);
|
||||
}
|
||||
pdf.new_page();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compliance PDF
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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("
|
||||
SELECT h.display_name, h.fqdn,
|
||||
COALESCE(pd.total_packages, 0) AS total_packages,
|
||||
COALESCE(pd.pending_patches, 0) AS pending_patches,
|
||||
CASE WHEN COALESCE(pd.total_packages,0) = 0 THEN 100.0
|
||||
ELSE ROUND((1.0 - pd.pending_patches::float / NULLIF(pd.total_packages,0)) * 100, 1)
|
||||
END AS compliance_pct,
|
||||
h.health_status::text AS health_status
|
||||
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")?
|
||||
} else {
|
||||
sqlx::query("
|
||||
SELECT h.display_name, h.fqdn,
|
||||
COALESCE(pd.total_packages, 0) AS total_packages,
|
||||
COALESCE(pd.pending_patches, 0) AS pending_patches,
|
||||
CASE WHEN COALESCE(pd.total_packages,0) = 0 THEN 100.0
|
||||
ELSE ROUND((1.0 - pd.pending_patches::float / NULLIF(pd.total_packages,0)) * 100, 1)
|
||||
END AS compliance_pct,
|
||||
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")?
|
||||
};
|
||||
|
||||
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, 195.0, 230.0];
|
||||
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 pending: 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(), &pending.to_string(), &format!("{:.1}%", pct), &status],
|
||||
col_x, 8.0, false,
|
||||
);
|
||||
}
|
||||
if !labels.is_empty() {
|
||||
match render_bar_chart(&labels, &values, "Compliance % by Host") {
|
||||
Ok(png) => {
|
||||
pdf.new_page();
|
||||
pdf.write_text("Compliance Chart", 16.0, MARGIN, 200.0, true);
|
||||
if let Err(e) = pdf.embed_image(&png, MARGIN, 10.0, 0.18, 0.18) {
|
||||
tracing::warn!(error = %e, "compliance chart embed failed");
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::warn!(error = %e, "compliance chart render failed"),
|
||||
}
|
||||
}
|
||||
pdf.save()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Patch history PDF
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn patch_history_pdf(
|
||||
pool: &sqlx::PgPool,
|
||||
params: &ReportParams,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
use sqlx::Row;
|
||||
|
||||
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,
|
||||
COALESCE(u.username, 'system') AS operator
|
||||
FROM patch_job_hosts pjh
|
||||
JOIN patch_jobs pj ON pj.id = pjh.job_id
|
||||
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 day_counts: std::collections::BTreeMap<String, f64> = std::collections::BTreeMap::new();
|
||||
for row in &rows {
|
||||
if let Ok(Some(started)) = row.try_get::<Option<chrono::DateTime<chrono::Utc>>, _>("started_at") {
|
||||
*day_counts.entry(started.format("%Y-%m-%d").to_string()).or_insert(0.0) += 1.0;
|
||||
}
|
||||
}
|
||||
let chart_labels: Vec<String> = day_counts.keys().cloned().collect();
|
||||
let chart_values: Vec<f64> = day_counts.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);
|
||||
for row in &rows {
|
||||
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);
|
||||
}
|
||||
if !chart_labels.is_empty() {
|
||||
match render_bar_chart(&chart_labels, &chart_values, "Jobs per Day") {
|
||||
Ok(png) => {
|
||||
pdf.new_page();
|
||||
pdf.write_text("Patch Activity Chart", 16.0, MARGIN, 200.0, true);
|
||||
if let Err(e) = pdf.embed_image(&png, MARGIN, 10.0, 0.18, 0.18) {
|
||||
tracing::warn!(error = %e, "patch history chart embed failed");
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::warn!(error = %e, "patch history chart render failed"),
|
||||
}
|
||||
}
|
||||
pdf.save()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vulnerability PDF
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn vulnerability_pdf(
|
||||
pool: &sqlx::PgPool,
|
||||
params: &ReportParams,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
use sqlx::Row;
|
||||
|
||||
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);
|
||||
|
||||
match sqlx::query("
|
||||
SELECT h.display_name, h.fqdn,
|
||||
cve.cve_id, cve.package_name, cve.severity, cve.available_version,
|
||||
pd.updated_at AS last_seen_at
|
||||
FROM hosts h
|
||||
JOIN host_patch_data pd ON pd.host_id = h.id
|
||||
CROSS JOIN LATERAL jsonb_to_recordset(COALESCE(pd.cve_data, '[]'::jsonb))
|
||||
AS cve(cve_id text, package_name text, severity text, available_version text)
|
||||
WHERE ($1::timestamptz IS NULL OR pd.updated_at >= $1)
|
||||
AND ($2::timestamptz IS NULL OR pd.updated_at <= $2)
|
||||
ORDER BY CASE cve.severity WHEN 'critical' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 ELSE 4 END, h.display_name")
|
||||
.bind(params.from).bind(params.to)
|
||||
.fetch_all(pool).await
|
||||
{
|
||||
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_id: 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_id,&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 available: {}", e), 10.0, MARGIN, y, false);
|
||||
}
|
||||
}
|
||||
pdf.save()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user