Stage 0: DT-API-Client (read-only) + Unit-Tests
This commit is contained in:
@@ -0,0 +1,94 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
/// Read-only Client fuer die Dependency-Track REST-API (v1).
|
||||||
|
///
|
||||||
|
/// Bewusst duenn: liefert die rohen JSON-Antworten als String zurueck. Das
|
||||||
|
/// Modellieren einzelner DT-Typen (serde-Structs) heben wir uns fuer spaeter
|
||||||
|
/// auf -- fuers erste reicht JSON-Durchreichen an den MCP-Client.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DtrackClient {
|
||||||
|
http: Client,
|
||||||
|
base_url: String,
|
||||||
|
api_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Haengt `/api/v1` an die Basis-URL an und entfernt einen ggf. vorhandenen
|
||||||
|
/// abschliessenden Slash. Separat gehalten, damit es ohne Netzwerk testbar ist.
|
||||||
|
pub fn normalize_base(base: &str) -> String {
|
||||||
|
format!("{}/api/v1", base.trim_end_matches('/'))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DtrackClient {
|
||||||
|
/// Baut den Client aus Umgebungsvariablen:
|
||||||
|
/// - `DTRACK_URL` Basis-URL der DT-Instanz (z.B. https://dtrack.firma.intern)
|
||||||
|
/// - `DTRACK_API_KEY` API-Key eines read-only Teams
|
||||||
|
/// - `DTRACK_INSECURE` optional "1"/"true" -> TLS-Zertpruefung aus
|
||||||
|
/// (nur als Notbehelf bei internem CA-Cert; nicht fuer Prod)
|
||||||
|
pub fn from_env() -> Result<Self> {
|
||||||
|
let base = std::env::var("DTRACK_URL").context("DTRACK_URL nicht gesetzt")?;
|
||||||
|
let api_key = std::env::var("DTRACK_API_KEY").context("DTRACK_API_KEY nicht gesetzt")?;
|
||||||
|
let insecure = std::env::var("DTRACK_INSECURE")
|
||||||
|
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let http = Client::builder()
|
||||||
|
.danger_accept_invalid_certs(insecure)
|
||||||
|
.build()
|
||||||
|
.context("HTTP-Client konnte nicht erstellt werden")?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
http,
|
||||||
|
base_url: normalize_base(&base),
|
||||||
|
api_key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, path: &str) -> Result<String> {
|
||||||
|
let url = format!("{}{}", self.base_url, path);
|
||||||
|
let resp = self
|
||||||
|
.http
|
||||||
|
.get(&url)
|
||||||
|
.header("X-Api-Key", &self.api_key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Request an {url} fehlgeschlagen"))?;
|
||||||
|
|
||||||
|
let status = resp.status();
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
if !status.is_success() {
|
||||||
|
anyhow::bail!("Dependency-Track {path} -> HTTP {status}: {body}");
|
||||||
|
}
|
||||||
|
Ok(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /project -- aktive Projekte (Name, Version, UUID ...).
|
||||||
|
pub async fn list_projects(&self) -> Result<String> {
|
||||||
|
self.get("/project?pageSize=100&excludeInactive=true").await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /finding/project/{uuid} -- Findings (Schwachstellen) eines Projekts.
|
||||||
|
pub async fn project_findings(&self, uuid: &str) -> Result<String> {
|
||||||
|
self.get(&format!("/finding/project/{uuid}")).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /metrics/project/{uuid}/current -- aktuelle Sicherheits-Metriken.
|
||||||
|
pub async fn project_metrics(&self, uuid: &str) -> Result<String> {
|
||||||
|
self.get(&format!("/metrics/project/{uuid}/current")).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::normalize_base;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_base_appends_api_path() {
|
||||||
|
assert_eq!(normalize_base("https://dt.example"), "https://dt.example/api/v1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_base_trims_trailing_slash() {
|
||||||
|
assert_eq!(normalize_base("https://dt.example/"), "https://dt.example/api/v1");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user