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