diff --git a/crates/dtrack-core/src/lib.rs b/crates/dtrack-core/src/lib.rs index 23855cf..8540fee 100644 --- a/crates/dtrack-core/src/lib.rs +++ b/crates/dtrack-core/src/lib.rs @@ -3,7 +3,11 @@ use reqwest::Client; pub use dtrack_config::DtrackConfig; -/// Read-only Client fuer die Dependency-Track REST-API (v1). +/// Client fuer die Dependency-Track REST-API (v1). +/// +/// Ueberwiegend read-only (Projekte, Findings, Metriken, VEX/SBOM ...). Einzige +/// Schreib-Operation ist `set_analysis` (Suppression/Analyse setzen) -- diese +/// braucht einen API-Key mit dem DT-Recht VULNERABILITY_ANALYSIS. /// /// Bewusst duenn: liefert die rohen JSON-Antworten als String zurueck. Das /// Modellieren einzelner DT-Typen (serde-Structs) heben wir uns fuer spaeter @@ -58,6 +62,26 @@ impl DtrackClient { .await .with_context(|| format!("Request an {url} fehlgeschlagen"))?; + self.handle(path, resp).await + } + + /// PUT mit JSON-Body. Einzige Schreib-Operation des Clients. + async fn put_json(&self, path: &str, body: serde_json::Value) -> Result { + let url = format!("{}{}", self.base_url, path); + let resp = self + .http + .put(&url) + .header("X-Api-Key", &self.api_key) + .json(&body) + .send() + .await + .with_context(|| format!("Request an {url} fehlgeschlagen"))?; + + self.handle(path, resp).await + } + + /// Gemeinsame Status-/Body-Behandlung fuer GET und PUT. + async fn handle(&self, path: &str, resp: reqwest::Response) -> Result { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); if !status.is_success() { @@ -114,6 +138,50 @@ impl DtrackClient { self.get(&format!("/bom/cyclonedx/project/{uuid}?format=json")) .await } + + /// PUT /analysis -- Analyse-/Suppression-Entscheidung fuer ein Finding setzen. + /// + /// Adressiert wird das Finding ueber das Tripel (project, component, + /// vulnerability) -- alle drei UUIDs stehen in der `project_findings`-Antwort. + /// `suppressed` schaltet die Unterdrueckung an/aus; `state`/`justification`/ + /// `comment` sind optionale Analyse-Felder (DT-Enums, z.B. state + /// `FALSE_POSITIVE`/`NOT_AFFECTED`). Braucht das DT-Recht VULNERABILITY_ANALYSIS. + #[allow(clippy::too_many_arguments)] + pub async fn set_analysis( + &self, + project: &str, + component: &str, + vulnerability: &str, + suppressed: Option, + state: Option<&str>, + justification: Option<&str>, + comment: Option<&str>, + ) -> Result { + let mut body = serde_json::json!({ + "project": project, + "component": component, + "vulnerability": vulnerability, + }); + let obj = body + .as_object_mut() + .expect("json! erzeugt immer ein Objekt"); + if let Some(s) = suppressed { + obj.insert("suppressed".into(), serde_json::Value::Bool(s)); + } + if let Some(s) = state { + obj.insert("analysisState".into(), serde_json::Value::String(s.into())); + } + if let Some(j) = justification { + obj.insert( + "analysisJustification".into(), + serde_json::Value::String(j.into()), + ); + } + if let Some(c) = comment { + obj.insert("comment".into(), serde_json::Value::String(c.into())); + } + self.put_json("/analysis", body).await + } } #[cfg(test)]