Skip to content

Commit a20bcc8

Browse files
Harden product security imports and reviews
1 parent ff69eec commit a20bcc8

3 files changed

Lines changed: 1309 additions & 50 deletions

File tree

rust/iscy-backend/src/lib.rs

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
487493
struct 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)]
691705
pub 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+
35643650
async 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+
}
92689521
fn 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

Comments
 (0)