@@ -483,6 +483,12 @@ struct WebControlStatusForm {
483483 notes : Option < String > ,
484484}
485485
486+ #[ derive( Debug , Deserialize ) ]
487+ struct WebCveCorrelationDecisionForm {
488+ status : String ,
489+ rationale : Option < String > ,
490+ }
491+
486492#[ derive( Debug , Deserialize ) ]
487493struct WebCveLlmTestForm {
488494 prompt : Option < String > ,
@@ -687,6 +693,14 @@ pub struct ProductSecurityCveCorrelationResponse {
687693 pub result : product_security_store:: ProductSecurityCveCorrelationResult ,
688694}
689695
696+ #[ derive( Debug , Serialize ) ]
697+ pub struct ProductSecurityCveCorrelationDecisionResponse {
698+ pub accepted : bool ,
699+ pub api_version : & ' static str ,
700+ #[ serde( flatten) ]
701+ pub result : product_security_store:: ProductSecurityCveCorrelationDecisionResult ,
702+ }
703+
690704#[ derive( Debug , Serialize ) ]
691705pub struct CveFeedResponse {
692706 pub api_version : & ' static str ,
@@ -3561,6 +3575,78 @@ async fn product_security_cve_correlations(
35613575 }
35623576}
35633577
3578+ async fn product_security_cve_correlation_update (
3579+ Path ( correlation_id) : Path < i64 > ,
3580+ State ( state) : State < AppState > ,
3581+ headers : HeaderMap ,
3582+ Json ( payload) : Json < product_security_store:: ProductSecurityCveCorrelationDecisionRequest > ,
3583+ ) -> Response {
3584+ let context = match authenticated_tenant_context ( & state, & headers) . await {
3585+ Ok ( context) => context,
3586+ Err ( err) => {
3587+ return (
3588+ err. status_code ( ) ,
3589+ Json ( ApiErrorResponse {
3590+ accepted : false ,
3591+ api_version : "v1" ,
3592+ error_code : err. error_code ( ) ,
3593+ message : err. message ( ) . to_string ( ) ,
3594+ } ) ,
3595+ )
3596+ . into_response ( ) ;
3597+ }
3598+ } ;
3599+ if let Some ( response) = write_permission_error ( & context) {
3600+ return response;
3601+ }
3602+ let Some ( store) = state. product_security_store else {
3603+ return (
3604+ StatusCode :: SERVICE_UNAVAILABLE ,
3605+ Json ( ApiErrorResponse {
3606+ accepted : false ,
3607+ api_version : "v1" ,
3608+ error_code : "database_not_configured" ,
3609+ message : "Rust-Product-Security-Store ist nicht konfiguriert." . to_string ( ) ,
3610+ } ) ,
3611+ )
3612+ . into_response ( ) ;
3613+ } ;
3614+ match store
3615+ . update_cve_correlation ( context. tenant_id , correlation_id, payload)
3616+ . await
3617+ {
3618+ Ok ( Some ( result) ) => (
3619+ StatusCode :: OK ,
3620+ Json ( ProductSecurityCveCorrelationDecisionResponse {
3621+ accepted : true ,
3622+ api_version : "v1" ,
3623+ result,
3624+ } ) ,
3625+ )
3626+ . into_response ( ) ,
3627+ Ok ( None ) => (
3628+ StatusCode :: NOT_FOUND ,
3629+ Json ( ApiErrorResponse {
3630+ accepted : false ,
3631+ api_version : "v1" ,
3632+ error_code : "product_security_cve_correlation_not_found" ,
3633+ message : "CVE-Asset-Korrelation wurde nicht gefunden." . to_string ( ) ,
3634+ } ) ,
3635+ )
3636+ . into_response ( ) ,
3637+ Err ( err) => (
3638+ StatusCode :: BAD_REQUEST ,
3639+ Json ( ApiErrorResponse {
3640+ accepted : false ,
3641+ api_version : "v1" ,
3642+ error_code : "invalid_product_security_cve_correlation" ,
3643+ message : format ! ( "CVE-Asset-Korrelation konnte nicht aktualisiert werden: {err}" ) ,
3644+ } ) ,
3645+ )
3646+ . into_response ( ) ,
3647+ }
3648+ }
3649+
35643650async fn risk_register ( State ( state) : State < AppState > , headers : HeaderMap ) -> Response {
35653651 let context = match authenticated_tenant_context ( & state, & headers) . await {
35663652 Ok ( context) => context,
@@ -8989,6 +9075,87 @@ async fn web_product_security(
89899075 } )
89909076 . collect :: < Vec < _ > > ( )
89919077 . join ( "" ) ;
9078+ let import_rows = overview
9079+ . import_artifacts
9080+ . iter ( )
9081+ . map ( |artifact| {
9082+ let errors = if artifact. validation_errors . is_empty ( ) {
9083+ "Keine" . to_string ( )
9084+ } else {
9085+ artifact
9086+ . validation_errors
9087+ . iter ( )
9088+ . map ( |error| html_escape ( error) )
9089+ . collect :: < Vec < _ > > ( )
9090+ . join ( "<br>" )
9091+ } ;
9092+ format ! (
9093+ r#"<tr><td>{}</td><td>{}</td><td>{}</td><td>{} {}</td><td>{}</td><td>{}</td><td>{}/{}</td><td>{}</td><td>{}</td></tr>"# ,
9094+ html_escape( & artifact. artifact_type) ,
9095+ html_escape( & artifact. file_name) ,
9096+ html_escape( artifact. product_name. as_deref( ) . unwrap_or( "Tenantweit" ) ) ,
9097+ html_escape( & artifact. format_name) ,
9098+ html_escape( & artifact. format_version) ,
9099+ html_escape( & artifact. validation_status) ,
9100+ errors,
9101+ artifact. matched_component_count,
9102+ artifact. component_count,
9103+ artifact. cve_count,
9104+ html_escape( & artifact. created_at) ,
9105+ )
9106+ } )
9107+ . collect :: < Vec < _ > > ( )
9108+ . join ( "" ) ;
9109+ let correlation_rows = overview
9110+ . cve_correlations
9111+ . iter ( )
9112+ . map ( |correlation| {
9113+ let actions = if can_write && correlation. status == "SUGGESTED" {
9114+ format ! (
9115+ r#"<form method="post" action="{}" class="inline-form">
9116+ <input type="hidden" name="status" value="ACCEPTED">
9117+ <input type="hidden" name="rationale" value="Fachlich akzeptiert im Product-Security-Review.">
9118+ <button type="submit">Akzeptieren</button>
9119+ </form>
9120+ <form method="post" action="{}" class="inline-form">
9121+ <input type="hidden" name="status" value="REJECTED">
9122+ <input type="hidden" name="rationale" value="Fachlich abgelehnt im Product-Security-Review.">
9123+ <button type="submit">Ablehnen</button>
9124+ </form>"# ,
9125+ web_path_with_context(
9126+ & format!(
9127+ "/product-security/cve-correlations/{}" ,
9128+ correlation. id
9129+ ) ,
9130+ Some ( & context) ,
9131+ ) ,
9132+ web_path_with_context(
9133+ & format!(
9134+ "/product-security/cve-correlations/{}" ,
9135+ correlation. id
9136+ ) ,
9137+ Some ( & context) ,
9138+ ) ,
9139+ )
9140+ } else {
9141+ "-" . to_string ( )
9142+ } ;
9143+ format ! (
9144+ r#"<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td><code>{}</code><br>{}</td><td>{}%</td><td>{}</td><td>{}</td><td>{}</td></tr>"# ,
9145+ html_escape( & correlation. cve) ,
9146+ html_escape( correlation. asset_name. as_deref( ) . unwrap_or( "-" ) ) ,
9147+ html_escape( correlation. product_name. as_deref( ) . unwrap_or( "-" ) ) ,
9148+ html_escape( correlation. component_name. as_deref( ) . unwrap_or( "-" ) ) ,
9149+ html_escape( & correlation. match_type) ,
9150+ html_escape( & correlation. match_value) ,
9151+ correlation. confidence,
9152+ html_escape( & correlation. status) ,
9153+ html_escape( & correlation. rationale) ,
9154+ actions,
9155+ )
9156+ } )
9157+ . collect :: < Vec < _ > > ( )
9158+ . join ( "" ) ;
89929159 let product_options = overview
89939160 . products
89949161 . iter ( )
@@ -9079,6 +9246,20 @@ async fn web_product_security(
90799246 </table>
90809247 </article>
90819248 {}
9249+ <article class="panel wide">
9250+ <h2>Import-Historie</h2>
9251+ <table>
9252+ <thead><tr><th>Typ</th><th>Datei</th><th>Produkt</th><th>Format</th><th>Status</th><th>Validierung</th><th>SBOM</th><th>CVEs</th><th>Zeit</th></tr></thead>
9253+ <tbody>{}</tbody>
9254+ </table>
9255+ </article>
9256+ <article class="panel wide">
9257+ <h2>CVE-Asset-Korrelationen</h2>
9258+ <table>
9259+ <thead><tr><th>CVE</th><th>Asset</th><th>Produkt</th><th>Komponente</th><th>Match</th><th>Confidence</th><th>Status</th><th>Rationale</th><th>Review</th></tr></thead>
9260+ <tbody>{}</tbody>
9261+ </table>
9262+ </article>
90829263 </section>
90839264 "# ,
90849265 overview. tenant_id,
@@ -9107,6 +9288,16 @@ async fn web_product_security(
91079288 snapshot_rows
91089289 } ,
91099290 import_panel,
9291+ if import_rows. is_empty( ) {
9292+ web_empty_row( 9 , "Noch keine CSAF-/SBOM-Importe vorhanden." )
9293+ } else {
9294+ import_rows
9295+ } ,
9296+ if correlation_rows. is_empty( ) {
9297+ web_empty_row( 9 , "Noch keine CVE-Asset-Korrelationen vorhanden." )
9298+ } else {
9299+ correlation_rows
9300+ } ,
91109301 ) ;
91119302 web_page (
91129303 "Product Security" ,
@@ -9265,6 +9456,68 @@ async fn web_product_security_cve_correlations_submit(
92659456 . into_response ( ) ,
92669457 }
92679458}
9459+
9460+ async fn web_product_security_cve_correlation_update (
9461+ Path ( correlation_id) : Path < i64 > ,
9462+ State ( state) : State < AppState > ,
9463+ headers : HeaderMap ,
9464+ Form ( form) : Form < WebCveCorrelationDecisionForm > ,
9465+ ) -> Response {
9466+ let auth_context = match authenticated_tenant_context ( & state, & headers) . await {
9467+ Ok ( context) => context,
9468+ Err ( _) => {
9469+ return web_missing_context ( "Product Security" , "/product-security/" ) . into_response ( )
9470+ }
9471+ } ;
9472+ let context = WebContext {
9473+ tenant_id : auth_context. tenant_id ,
9474+ user_id : auth_context. user_id ,
9475+ user_email : auth_context. user_email . clone ( ) ,
9476+ } ;
9477+ if !auth_context. can_write ( ) {
9478+ return web_error_page (
9479+ "Product Security" ,
9480+ "/product-security/" ,
9481+ & context,
9482+ "Diese Rust-Webroute benoetigt eine schreibende ISCY-Rolle." ,
9483+ )
9484+ . into_response ( ) ;
9485+ }
9486+ let Some ( store) = state. product_security_store else {
9487+ return web_store_missing (
9488+ "Product Security" ,
9489+ "/product-security/" ,
9490+ & context,
9491+ "Product Security" ,
9492+ )
9493+ . into_response ( ) ;
9494+ } ;
9495+ let payload = product_security_store:: ProductSecurityCveCorrelationDecisionRequest {
9496+ status : form. status ,
9497+ rationale : form. rationale ,
9498+ } ;
9499+ match store
9500+ . update_cve_correlation ( auth_context. tenant_id , correlation_id, payload)
9501+ . await
9502+ {
9503+ Ok ( Some ( _) ) => Redirect :: to ( & web_path_with_context ( "/product-security/" , Some ( & context) ) )
9504+ . into_response ( ) ,
9505+ Ok ( None ) => web_error_page (
9506+ "Product Security" ,
9507+ "/product-security/" ,
9508+ & context,
9509+ "CVE-Asset-Korrelation wurde nicht gefunden." ,
9510+ )
9511+ . into_response ( ) ,
9512+ Err ( err) => web_error_page (
9513+ "Product Security" ,
9514+ "/product-security/" ,
9515+ & context,
9516+ & err. to_string ( ) ,
9517+ )
9518+ . into_response ( ) ,
9519+ }
9520+ }
92689521fn selected_attr ( selected : bool ) -> & ' static str {
92699522 if selected {
92709523 " selected"
@@ -14055,6 +14308,10 @@ pub fn app_router_with_state(state: AppState) -> Router {
1405514308 "/api/v1/product-security/cve-correlations" ,
1405614309 post ( product_security_cve_correlations) ,
1405714310 )
14311+ . route (
14312+ "/api/v1/product-security/cve-correlations/{correlation_id}" ,
14313+ patch ( product_security_cve_correlation_update) ,
14314+ )
1405814315 . route ( "/api/v1/risks" , get ( risk_register) . post ( risk_create) )
1405914316 . route (
1406014317 "/api/v1/risks/{risk_id}" ,
@@ -14231,6 +14488,10 @@ pub fn app_router_with_state(state: AppState) -> Router {
1423114488 "/product-security/cve-correlations" ,
1423214489 post ( web_product_security_cve_correlations_submit) ,
1423314490 )
14491+ . route (
14492+ "/product-security/cve-correlations/{correlation_id}" ,
14493+ post ( web_product_security_cve_correlation_update) ,
14494+ )
1423414495 . route ( "/cves/" , get ( web_cves) . post ( web_cves_submit) )
1423514496 . route (
1423614497 "/cves/llm-test/" ,
0 commit comments