Stage 0: DT-API-Client (read-only) + Unit-Tests

This commit is contained in:
2026-06-22 08:02:30 +00:00
parent fb3906e821
commit 71889ab036
+94
View File
@@ -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");
}
}