Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ parking_lot = "0.12"
peak_alloc = "0.3.0"
pem = "3"
petgraph = { version = "0.8.0", features = ["serde-1"] }
printpdf = "0.7"
quick-xml = "0.39.0"
rand = "0.10.0"
regex = "1.10.3"
Expand Down
2 changes: 2 additions & 0 deletions entity/src/risk_assessment_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub struct Model {
pub processed: bool,

pub uploaded_at: OffsetDateTime,

pub risk_prioritization: Option<serde_json::Value>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
Expand Down
2 changes: 2 additions & 0 deletions migration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ mod m0002140_p2p_right_index;
mod m0002150_fix_advisory_labels_index;
mod m0002160_fix_ref_fk;
mod m0002170_create_risk_assessment;
mod m0002180_add_risk_prioritization;

pub trait MigratorExt: Send {
fn build_migrations() -> Migrations;
Expand Down Expand Up @@ -128,6 +129,7 @@ impl MigratorExt for Migrator {
.normal(m0002150_fix_advisory_labels_index::Migration)
.normal(m0002160_fix_ref_fk::Migration)
.normal(m0002170_create_risk_assessment::Migration)
.normal(m0002180_add_risk_prioritization::Migration)
}
}

Expand Down
41 changes: 41 additions & 0 deletions migration/src/m0002180_add_risk_prioritization.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(RiskAssessmentDocument::Table)
.add_column_if_not_exists(
ColumnDef::new(RiskAssessmentDocument::RiskPrioritization).json_binary(),
)
.to_owned(),
)
.await?;

Ok(())
}

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(RiskAssessmentDocument::Table)
.drop_column(RiskAssessmentDocument::RiskPrioritization)
.to_owned(),
)
.await?;

Ok(())
}
}

#[derive(DeriveIden)]
enum RiskAssessmentDocument {
Table,
RiskPrioritization,
}
1 change: 1 addition & 0 deletions modules/fundamental/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ hex = { workspace = true }
isx = { workspace = true }
itertools = { workspace = true }
log = { workspace = true }
printpdf = { workspace = true }
sanitize-filename = { workspace = true }
sea-orm = { workspace = true }
sea-query = { workspace = true }
Expand Down
48 changes: 46 additions & 2 deletions modules/fundamental/src/risk_assessment/endpoints/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#[cfg(test)]
mod test;

use super::{model::*, service::RiskAssessmentService};
use super::{model::*, service::RiskAssessmentService, service::report_generator};
use crate::{Error, db::DatabaseExt};
use actix_web::{HttpRequest, HttpResponse, Responder, delete, get, post, web};
use futures_util::stream::TryStreamExt;
Expand Down Expand Up @@ -31,7 +31,8 @@ pub fn configure(
.service(delete_assessment)
.service(upload_document)
.service(download_document)
.service(get_results);
.service(get_results)
.service(generate_report);
}

#[utoipa::path(
Expand Down Expand Up @@ -302,3 +303,46 @@ async fn get_results(
None => HttpResponse::NotFound().finish(),
})
}

#[utoipa::path(
tag = "risk-assessment",
operation_id = "generateRiskAssessmentReport",
params(
("id", Path, description = "The ID of the risk assessment"),
),
responses(
(status = 200, description = "The generated PDF report", content_type = "application/pdf"),
(status = 400, description = "The request was not valid"),
(status = 401, description = "The user was not authenticated"),
(status = 403, description = "The user authenticated, but not authorized for this operation"),
(status = 404, description = "The risk assessment was not found"),
)
)]
#[get("/v2/risk-assessment/{id}/report")]
/// Generate and download a PDF report for a risk assessment
async fn generate_report(
service: web::Data<RiskAssessmentService>,
db: web::Data<Database>,
id: web::Path<String>,
_: Require<ReadRiskAssessment>,
) -> Result<impl Responder, Error> {
let tx = db.begin_read().await?;
let report_data = service.get_report_data(&id, &tx).await?;

let Some(report_data) = report_data else {
return Ok(HttpResponse::NotFound().finish());
};

let pdf_bytes = report_generator::generate_report(&report_data).map_err(|e| {
log::error!("Failed to generate PDF report for assessment {}: {e}", &*id);
Error::Internal("Failed to generate PDF report".to_string())
})?;

Ok(HttpResponse::Ok()
.content_type("application/pdf")
.append_header((
"Content-Disposition",
format!("attachment; filename=\"risk-assessment-{}.pdf\"", &*id),
))
.body(pdf_bytes))
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ pub struct RiskAssessmentEntry {
pub struct SarEvaluationResponse {
pub criteria_assessments: Vec<CriterionAssessment>,
pub risk_assessments: Vec<RiskAssessmentEntry>,
// risk_prioritization is present in the schema but not stored per-criterion.
/// Risk prioritization data from the LLM (summary, critical gaps, top risks).
pub risk_prioritization: Option<serde_json::Value>,
}

/// Extract text content from a PDF file.
Expand Down
Loading