From ba23ca35297c273e3747333897fb8dddb81d8377 Mon Sep 17 00:00:00 2001 From: Ismail Pelaseyed Date: Mon, 9 Mar 2026 12:55:39 +0100 Subject: [PATCH] feat: add --tolerance, --refresh, --mode, and --format flags to check command Support the new API query parameters: tolerance (conservative/lenient/yolo), refresh (force fresh scan), mode (full for synchronous scan), and format (json/simple/badge). Introduce CheckOptions struct to group parameters. Bump version to 0.1.14. Made-with: Cursor --- Cargo.toml | 2 +- crates/cli/src/api_client.rs | 209 +++++++++++++++++++++++++++---- crates/cli/src/commands/check.rs | 7 +- crates/cli/src/main.rs | 33 ++++- package.json | 2 +- 5 files changed, 223 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a1275bf..330900a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = [ ] [workspace.package] -version = "0.1.13" +version = "0.1.14" edition = "2021" authors = ["brin contributors"] license = "MIT" diff --git a/crates/cli/src/api_client.rs b/crates/cli/src/api_client.rs index 69c1ac4..acf0e95 100644 --- a/crates/cli/src/api_client.rs +++ b/crates/cli/src/api_client.rs @@ -21,6 +21,17 @@ pub struct CheckResult { pub headers: BrinHeaders, } +/// Optional query parameters for a check request +#[derive(Debug, Default)] +pub struct CheckOptions<'a> { + pub details: bool, + pub webhook: Option<&'a str>, + pub tolerance: Option<&'a str>, + pub refresh: bool, + pub mode: Option<&'a str>, + pub format: Option<&'a str>, +} + /// Client for the brin API pub struct BrinClient { client: Client, @@ -43,24 +54,34 @@ impl BrinClient { /// /// - `origin` — e.g. `"npm"`, `"pypi"`, `"repo"`, `"mcp"`, `"skill"`, `"domain"`, `"commit"` /// - `identifier` — the artifact identifier, e.g. `"express"`, `"owner/repo"`, `"owner/repo@sha"` - /// - `details` — if true, appends `?details=true` to include sub-scores - /// - `webhook` — if provided, appends `?webhook=` so the API POSTs tier events + /// - `opts` — optional query parameters (details, webhook, tolerance, refresh, mode, format) pub async fn check( &self, origin: &str, identifier: &str, - details: bool, - webhook: Option<&str>, + opts: &CheckOptions<'_>, ) -> Result { let url = format!("{}/{}/{}", self.base_url, origin, identifier); let mut query: Vec<(&str, String)> = Vec::new(); - if details { + if opts.details { query.push(("details", "true".into())); } - if let Some(wh) = webhook { + if let Some(wh) = opts.webhook { query.push(("webhook", wh.to_string())); } + if let Some(t) = opts.tolerance { + query.push(("tolerance", t.to_string())); + } + if opts.refresh { + query.push(("refresh", "true".into())); + } + if let Some(m) = opts.mode { + query.push(("mode", m.to_string())); + } + if let Some(f) = opts.format { + query.push(("format", f.to_string())); + } let response = self .client @@ -168,7 +189,10 @@ mod tests { .await; let client = BrinClient::new(&server.uri()); - let result = client.check("npm", "express", false, None).await.unwrap(); + let result = client + .check("npm", "express", &CheckOptions::default()) + .await + .unwrap(); // body is valid JSON containing expected fields let v: serde_json::Value = serde_json::from_str(&result.body).unwrap(); @@ -200,7 +224,7 @@ mod tests { let client = BrinClient::new(&server.uri()); let result = client - .check("repo", "expressjs/express", false, None) + .check("repo", "expressjs/express", &CheckOptions::default()) .await .unwrap(); @@ -227,7 +251,7 @@ mod tests { let client = BrinClient::new(&server.uri()); let result = client - .check("npm", "lodash@4.17.21", false, None) + .check("npm", "lodash@4.17.21", &CheckOptions::default()) .await .unwrap(); @@ -250,7 +274,11 @@ mod tests { .await; let client = BrinClient::new(&server.uri()); - let result = client.check("npm", "express", true, None).await.unwrap(); + let opts = CheckOptions { + details: true, + ..Default::default() + }; + let result = client.check("npm", "express", &opts).await.unwrap(); let v: serde_json::Value = serde_json::from_str(&result.body).unwrap(); assert!( @@ -274,8 +302,10 @@ mod tests { .await; let client = BrinClient::new(&server.uri()); - // details=false — should succeed without the query param being required - let result = client.check("npm", "express", false, None).await.unwrap(); + let result = client + .check("npm", "express", &CheckOptions::default()) + .await + .unwrap(); let v: serde_json::Value = serde_json::from_str(&result.body).unwrap(); assert!(v["sub_scores"].is_null() || !v.as_object().unwrap().contains_key("sub_scores")); } @@ -294,10 +324,11 @@ mod tests { .await; let client = BrinClient::new(&server.uri()); - let result = client - .check("npm", "express", false, Some("https://my-server.com/cb")) - .await - .unwrap(); + let opts = CheckOptions { + webhook: Some("https://my-server.com/cb"), + ..Default::default() + }; + let result = client.check("npm", "express", &opts).await.unwrap(); let v: serde_json::Value = serde_json::from_str(&result.body).unwrap(); assert_eq!(v["verdict"], "safe"); @@ -316,10 +347,12 @@ mod tests { .await; let client = BrinClient::new(&server.uri()); - let result = client - .check("npm", "express", true, Some("https://my-server.com/cb")) - .await - .unwrap(); + let opts = CheckOptions { + details: true, + webhook: Some("https://my-server.com/cb"), + ..Default::default() + }; + let result = client.check("npm", "express", &opts).await.unwrap(); let v: serde_json::Value = serde_json::from_str(&result.body).unwrap(); assert!(v["sub_scores"].is_object()); @@ -339,7 +372,10 @@ mod tests { .await; let client = BrinClient::new(&server.uri()); - let result = client.check("npm", "express", false, None).await.unwrap(); + let result = client + .check("npm", "express", &CheckOptions::default()) + .await + .unwrap(); assert!(result.headers.score.is_none()); assert!(result.headers.verdict.is_none()); @@ -361,7 +397,7 @@ mod tests { let client = BrinClient::new(&server.uri()); let err = client - .check("npm", "nonexistent", false, None) + .check("npm", "nonexistent", &CheckOptions::default()) .await .unwrap_err(); @@ -383,10 +419,137 @@ mod tests { let client = BrinClient::new(&server.uri()); let err = client - .check("npm", "express", false, None) + .check("npm", "express", &CheckOptions::default()) .await .unwrap_err(); assert!(err.to_string().contains("error")); } + + // ── check — ?tolerance= ─────────────────────────────────────── + + #[tokio::test] + async fn check_tolerance_appends_query_param() { + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/npm/express")) + .and(query_param("tolerance", "lenient")) + .respond_with(ResponseTemplate::new(200).set_body_json(safe_body())) + .mount(&server) + .await; + + let client = BrinClient::new(&server.uri()); + let opts = CheckOptions { + tolerance: Some("lenient"), + ..Default::default() + }; + let result = client.check("npm", "express", &opts).await.unwrap(); + + let v: serde_json::Value = serde_json::from_str(&result.body).unwrap(); + assert_eq!(v["verdict"], "safe"); + } + + // ── check — ?refresh=true ──────────────────────────────────────────── + + #[tokio::test] + async fn check_refresh_appends_query_param() { + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/npm/express")) + .and(query_param("refresh", "true")) + .respond_with(ResponseTemplate::new(200).set_body_json(safe_body())) + .mount(&server) + .await; + + let client = BrinClient::new(&server.uri()); + let opts = CheckOptions { + refresh: true, + ..Default::default() + }; + let result = client.check("npm", "express", &opts).await.unwrap(); + + let v: serde_json::Value = serde_json::from_str(&result.body).unwrap(); + assert_eq!(v["verdict"], "safe"); + } + + // ── check — ?mode=full ─────────────────────────────────────────────── + + #[tokio::test] + async fn check_mode_appends_query_param() { + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/npm/express")) + .and(query_param("mode", "full")) + .respond_with(ResponseTemplate::new(200).set_body_json(safe_body())) + .mount(&server) + .await; + + let client = BrinClient::new(&server.uri()); + let opts = CheckOptions { + mode: Some("full"), + ..Default::default() + }; + let result = client.check("npm", "express", &opts).await.unwrap(); + + let v: serde_json::Value = serde_json::from_str(&result.body).unwrap(); + assert_eq!(v["verdict"], "safe"); + } + + // ── check — ?format= ──────────────────────────────────────────── + + #[tokio::test] + async fn check_format_appends_query_param() { + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/npm/express")) + .and(query_param("format", "simple")) + .respond_with(ResponseTemplate::new(200).set_body_string("safe 85")) + .mount(&server) + .await; + + let client = BrinClient::new(&server.uri()); + let opts = CheckOptions { + format: Some("simple"), + ..Default::default() + }; + let result = client.check("npm", "express", &opts).await.unwrap(); + + assert_eq!(result.body, "safe 85"); + } + + // ── check — all new params combined ────────────────────────────────── + + #[tokio::test] + async fn check_all_new_params_combined() { + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/npm/express")) + .and(query_param("details", "true")) + .and(query_param("tolerance", "yolo")) + .and(query_param("refresh", "true")) + .and(query_param("mode", "full")) + .and(query_param("format", "json")) + .respond_with(ResponseTemplate::new(200).set_body_json(safe_body_with_sub_scores())) + .mount(&server) + .await; + + let client = BrinClient::new(&server.uri()); + let opts = CheckOptions { + details: true, + tolerance: Some("yolo"), + refresh: true, + mode: Some("full"), + format: Some("json"), + ..Default::default() + }; + let result = client.check("npm", "express", &opts).await.unwrap(); + + let v: serde_json::Value = serde_json::from_str(&result.body).unwrap(); + assert!(v["sub_scores"].is_object()); + } } diff --git a/crates/cli/src/commands/check.rs b/crates/cli/src/commands/check.rs index a4a9cb9..9785881 100644 --- a/crates/cli/src/commands/check.rs +++ b/crates/cli/src/commands/check.rs @@ -1,6 +1,6 @@ //! check command — look up an artifact's security assessment -use crate::api_client::BrinClient; +use crate::api_client::{BrinClient, CheckOptions}; use anyhow::{bail, Result}; /// Parse `/` from the artifact string. @@ -36,13 +36,12 @@ pub(crate) fn parse_artifact(artifact: &str) -> Result<(&str, &str)> { pub async fn run( client: &BrinClient, artifact: &str, - details: bool, - webhook: Option<&str>, + opts: &CheckOptions<'_>, headers: bool, ) -> Result<()> { let (origin, identifier) = parse_artifact(artifact)?; - let result = client.check(origin, identifier, details, webhook).await?; + let result = client.check(origin, identifier, opts).await?; if headers { // Print only the X-Brin-* response headers, one per line diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 097d609..9301063 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -3,6 +3,7 @@ mod api_client; mod commands; +use api_client::CheckOptions; use clap::{Parser, Subcommand}; #[derive(Parser)] @@ -48,6 +49,22 @@ enum Commands { #[arg(long, value_name = "URL")] webhook: Option, + /// Safety tolerance: conservative (default), lenient, or yolo + #[arg(long, value_name = "LEVEL")] + tolerance: Option, + + /// Force a fresh scan, ignoring any cached result + #[arg(long)] + refresh: bool, + + /// Scan mode: omit for async (returns preliminary score), or "full" for synchronous complete scan + #[arg(long, value_name = "MODE")] + mode: Option, + + /// Response format: json (default), simple, or badge + #[arg(long, value_name = "FORMAT")] + format: Option, + /// Print only the X-Brin-* response headers instead of the JSON body #[arg(long)] headers: bool, @@ -64,7 +81,21 @@ async fn main() -> anyhow::Result<()> { artifact, details, webhook, + tolerance, + refresh, + mode, + format, headers, - } => commands::check::run(&client, &artifact, details, webhook.as_deref(), headers).await, + } => { + let opts = CheckOptions { + details, + webhook: webhook.as_deref(), + tolerance: tolerance.as_deref(), + refresh, + mode: mode.as_deref(), + format: format.as_deref(), + }; + commands::check::run(&client, &artifact, &opts, headers).await + } } } diff --git a/package.json b/package.json index d07133d..b232f1e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "brin", - "version": "0.1.13", + "version": "0.1.14", "description": "the credit score for context — security scanning for packages, repos, MCP servers, skills, domains and commits", "bin": { "brin": "./bin/brin.js"