Skip to content

Commit 8c6652f

Browse files
authored
feat: add --tolerance, --refresh, --mode, and --format flags to check command (#47)
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
1 parent 2eee722 commit 8c6652f

File tree

5 files changed

+223
-30
lines changed

5 files changed

+223
-30
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ members = [
55
]
66

77
[workspace.package]
8-
version = "0.1.13"
8+
version = "0.1.14"
99
edition = "2021"
1010
authors = ["brin contributors"]
1111
license = "MIT"

crates/cli/src/api_client.rs

Lines changed: 186 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ pub struct CheckResult {
2121
pub headers: BrinHeaders,
2222
}
2323

24+
/// Optional query parameters for a check request
25+
#[derive(Debug, Default)]
26+
pub struct CheckOptions<'a> {
27+
pub details: bool,
28+
pub webhook: Option<&'a str>,
29+
pub tolerance: Option<&'a str>,
30+
pub refresh: bool,
31+
pub mode: Option<&'a str>,
32+
pub format: Option<&'a str>,
33+
}
34+
2435
/// Client for the brin API
2536
pub struct BrinClient {
2637
client: Client,
@@ -43,24 +54,34 @@ impl BrinClient {
4354
///
4455
/// - `origin` — e.g. `"npm"`, `"pypi"`, `"repo"`, `"mcp"`, `"skill"`, `"domain"`, `"commit"`
4556
/// - `identifier` — the artifact identifier, e.g. `"express"`, `"owner/repo"`, `"owner/repo@sha"`
46-
/// - `details` — if true, appends `?details=true` to include sub-scores
47-
/// - `webhook` — if provided, appends `?webhook=<url>` so the API POSTs tier events
57+
/// - `opts` — optional query parameters (details, webhook, tolerance, refresh, mode, format)
4858
pub async fn check(
4959
&self,
5060
origin: &str,
5161
identifier: &str,
52-
details: bool,
53-
webhook: Option<&str>,
62+
opts: &CheckOptions<'_>,
5463
) -> Result<CheckResult> {
5564
let url = format!("{}/{}/{}", self.base_url, origin, identifier);
5665

5766
let mut query: Vec<(&str, String)> = Vec::new();
58-
if details {
67+
if opts.details {
5968
query.push(("details", "true".into()));
6069
}
61-
if let Some(wh) = webhook {
70+
if let Some(wh) = opts.webhook {
6271
query.push(("webhook", wh.to_string()));
6372
}
73+
if let Some(t) = opts.tolerance {
74+
query.push(("tolerance", t.to_string()));
75+
}
76+
if opts.refresh {
77+
query.push(("refresh", "true".into()));
78+
}
79+
if let Some(m) = opts.mode {
80+
query.push(("mode", m.to_string()));
81+
}
82+
if let Some(f) = opts.format {
83+
query.push(("format", f.to_string()));
84+
}
6485

6586
let response = self
6687
.client
@@ -168,7 +189,10 @@ mod tests {
168189
.await;
169190

170191
let client = BrinClient::new(&server.uri());
171-
let result = client.check("npm", "express", false, None).await.unwrap();
192+
let result = client
193+
.check("npm", "express", &CheckOptions::default())
194+
.await
195+
.unwrap();
172196

173197
// body is valid JSON containing expected fields
174198
let v: serde_json::Value = serde_json::from_str(&result.body).unwrap();
@@ -200,7 +224,7 @@ mod tests {
200224

201225
let client = BrinClient::new(&server.uri());
202226
let result = client
203-
.check("repo", "expressjs/express", false, None)
227+
.check("repo", "expressjs/express", &CheckOptions::default())
204228
.await
205229
.unwrap();
206230

@@ -227,7 +251,7 @@ mod tests {
227251

228252
let client = BrinClient::new(&server.uri());
229253
let result = client
230-
.check("npm", "lodash@4.17.21", false, None)
254+
.check("npm", "lodash@4.17.21", &CheckOptions::default())
231255
.await
232256
.unwrap();
233257

@@ -250,7 +274,11 @@ mod tests {
250274
.await;
251275

252276
let client = BrinClient::new(&server.uri());
253-
let result = client.check("npm", "express", true, None).await.unwrap();
277+
let opts = CheckOptions {
278+
details: true,
279+
..Default::default()
280+
};
281+
let result = client.check("npm", "express", &opts).await.unwrap();
254282

255283
let v: serde_json::Value = serde_json::from_str(&result.body).unwrap();
256284
assert!(
@@ -274,8 +302,10 @@ mod tests {
274302
.await;
275303

276304
let client = BrinClient::new(&server.uri());
277-
// details=false — should succeed without the query param being required
278-
let result = client.check("npm", "express", false, None).await.unwrap();
305+
let result = client
306+
.check("npm", "express", &CheckOptions::default())
307+
.await
308+
.unwrap();
279309
let v: serde_json::Value = serde_json::from_str(&result.body).unwrap();
280310
assert!(v["sub_scores"].is_null() || !v.as_object().unwrap().contains_key("sub_scores"));
281311
}
@@ -294,10 +324,11 @@ mod tests {
294324
.await;
295325

296326
let client = BrinClient::new(&server.uri());
297-
let result = client
298-
.check("npm", "express", false, Some("https://my-server.com/cb"))
299-
.await
300-
.unwrap();
327+
let opts = CheckOptions {
328+
webhook: Some("https://my-server.com/cb"),
329+
..Default::default()
330+
};
331+
let result = client.check("npm", "express", &opts).await.unwrap();
301332

302333
let v: serde_json::Value = serde_json::from_str(&result.body).unwrap();
303334
assert_eq!(v["verdict"], "safe");
@@ -316,10 +347,12 @@ mod tests {
316347
.await;
317348

318349
let client = BrinClient::new(&server.uri());
319-
let result = client
320-
.check("npm", "express", true, Some("https://my-server.com/cb"))
321-
.await
322-
.unwrap();
350+
let opts = CheckOptions {
351+
details: true,
352+
webhook: Some("https://my-server.com/cb"),
353+
..Default::default()
354+
};
355+
let result = client.check("npm", "express", &opts).await.unwrap();
323356

324357
let v: serde_json::Value = serde_json::from_str(&result.body).unwrap();
325358
assert!(v["sub_scores"].is_object());
@@ -339,7 +372,10 @@ mod tests {
339372
.await;
340373

341374
let client = BrinClient::new(&server.uri());
342-
let result = client.check("npm", "express", false, None).await.unwrap();
375+
let result = client
376+
.check("npm", "express", &CheckOptions::default())
377+
.await
378+
.unwrap();
343379

344380
assert!(result.headers.score.is_none());
345381
assert!(result.headers.verdict.is_none());
@@ -361,7 +397,7 @@ mod tests {
361397

362398
let client = BrinClient::new(&server.uri());
363399
let err = client
364-
.check("npm", "nonexistent", false, None)
400+
.check("npm", "nonexistent", &CheckOptions::default())
365401
.await
366402
.unwrap_err();
367403

@@ -383,10 +419,137 @@ mod tests {
383419

384420
let client = BrinClient::new(&server.uri());
385421
let err = client
386-
.check("npm", "express", false, None)
422+
.check("npm", "express", &CheckOptions::default())
387423
.await
388424
.unwrap_err();
389425

390426
assert!(err.to_string().contains("error"));
391427
}
428+
429+
// ── check — ?tolerance=<level> ───────────────────────────────────────
430+
431+
#[tokio::test]
432+
async fn check_tolerance_appends_query_param() {
433+
let server = MockServer::start().await;
434+
435+
Mock::given(method("GET"))
436+
.and(path("/npm/express"))
437+
.and(query_param("tolerance", "lenient"))
438+
.respond_with(ResponseTemplate::new(200).set_body_json(safe_body()))
439+
.mount(&server)
440+
.await;
441+
442+
let client = BrinClient::new(&server.uri());
443+
let opts = CheckOptions {
444+
tolerance: Some("lenient"),
445+
..Default::default()
446+
};
447+
let result = client.check("npm", "express", &opts).await.unwrap();
448+
449+
let v: serde_json::Value = serde_json::from_str(&result.body).unwrap();
450+
assert_eq!(v["verdict"], "safe");
451+
}
452+
453+
// ── check — ?refresh=true ────────────────────────────────────────────
454+
455+
#[tokio::test]
456+
async fn check_refresh_appends_query_param() {
457+
let server = MockServer::start().await;
458+
459+
Mock::given(method("GET"))
460+
.and(path("/npm/express"))
461+
.and(query_param("refresh", "true"))
462+
.respond_with(ResponseTemplate::new(200).set_body_json(safe_body()))
463+
.mount(&server)
464+
.await;
465+
466+
let client = BrinClient::new(&server.uri());
467+
let opts = CheckOptions {
468+
refresh: true,
469+
..Default::default()
470+
};
471+
let result = client.check("npm", "express", &opts).await.unwrap();
472+
473+
let v: serde_json::Value = serde_json::from_str(&result.body).unwrap();
474+
assert_eq!(v["verdict"], "safe");
475+
}
476+
477+
// ── check — ?mode=full ───────────────────────────────────────────────
478+
479+
#[tokio::test]
480+
async fn check_mode_appends_query_param() {
481+
let server = MockServer::start().await;
482+
483+
Mock::given(method("GET"))
484+
.and(path("/npm/express"))
485+
.and(query_param("mode", "full"))
486+
.respond_with(ResponseTemplate::new(200).set_body_json(safe_body()))
487+
.mount(&server)
488+
.await;
489+
490+
let client = BrinClient::new(&server.uri());
491+
let opts = CheckOptions {
492+
mode: Some("full"),
493+
..Default::default()
494+
};
495+
let result = client.check("npm", "express", &opts).await.unwrap();
496+
497+
let v: serde_json::Value = serde_json::from_str(&result.body).unwrap();
498+
assert_eq!(v["verdict"], "safe");
499+
}
500+
501+
// ── check — ?format=<fmt> ────────────────────────────────────────────
502+
503+
#[tokio::test]
504+
async fn check_format_appends_query_param() {
505+
let server = MockServer::start().await;
506+
507+
Mock::given(method("GET"))
508+
.and(path("/npm/express"))
509+
.and(query_param("format", "simple"))
510+
.respond_with(ResponseTemplate::new(200).set_body_string("safe 85"))
511+
.mount(&server)
512+
.await;
513+
514+
let client = BrinClient::new(&server.uri());
515+
let opts = CheckOptions {
516+
format: Some("simple"),
517+
..Default::default()
518+
};
519+
let result = client.check("npm", "express", &opts).await.unwrap();
520+
521+
assert_eq!(result.body, "safe 85");
522+
}
523+
524+
// ── check — all new params combined ──────────────────────────────────
525+
526+
#[tokio::test]
527+
async fn check_all_new_params_combined() {
528+
let server = MockServer::start().await;
529+
530+
Mock::given(method("GET"))
531+
.and(path("/npm/express"))
532+
.and(query_param("details", "true"))
533+
.and(query_param("tolerance", "yolo"))
534+
.and(query_param("refresh", "true"))
535+
.and(query_param("mode", "full"))
536+
.and(query_param("format", "json"))
537+
.respond_with(ResponseTemplate::new(200).set_body_json(safe_body_with_sub_scores()))
538+
.mount(&server)
539+
.await;
540+
541+
let client = BrinClient::new(&server.uri());
542+
let opts = CheckOptions {
543+
details: true,
544+
tolerance: Some("yolo"),
545+
refresh: true,
546+
mode: Some("full"),
547+
format: Some("json"),
548+
..Default::default()
549+
};
550+
let result = client.check("npm", "express", &opts).await.unwrap();
551+
552+
let v: serde_json::Value = serde_json::from_str(&result.body).unwrap();
553+
assert!(v["sub_scores"].is_object());
554+
}
392555
}

crates/cli/src/commands/check.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! check command — look up an artifact's security assessment
22
3-
use crate::api_client::BrinClient;
3+
use crate::api_client::{BrinClient, CheckOptions};
44
use anyhow::{bail, Result};
55

66
/// Parse `<origin>/<identifier>` from the artifact string.
@@ -36,13 +36,12 @@ pub(crate) fn parse_artifact(artifact: &str) -> Result<(&str, &str)> {
3636
pub async fn run(
3737
client: &BrinClient,
3838
artifact: &str,
39-
details: bool,
40-
webhook: Option<&str>,
39+
opts: &CheckOptions<'_>,
4140
headers: bool,
4241
) -> Result<()> {
4342
let (origin, identifier) = parse_artifact(artifact)?;
4443

45-
let result = client.check(origin, identifier, details, webhook).await?;
44+
let result = client.check(origin, identifier, opts).await?;
4645

4746
if headers {
4847
// Print only the X-Brin-* response headers, one per line

crates/cli/src/main.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
mod api_client;
44
mod commands;
55

6+
use api_client::CheckOptions;
67
use clap::{Parser, Subcommand};
78

89
#[derive(Parser)]
@@ -48,6 +49,22 @@ enum Commands {
4849
#[arg(long, value_name = "URL")]
4950
webhook: Option<String>,
5051

52+
/// Safety tolerance: conservative (default), lenient, or yolo
53+
#[arg(long, value_name = "LEVEL")]
54+
tolerance: Option<String>,
55+
56+
/// Force a fresh scan, ignoring any cached result
57+
#[arg(long)]
58+
refresh: bool,
59+
60+
/// Scan mode: omit for async (returns preliminary score), or "full" for synchronous complete scan
61+
#[arg(long, value_name = "MODE")]
62+
mode: Option<String>,
63+
64+
/// Response format: json (default), simple, or badge
65+
#[arg(long, value_name = "FORMAT")]
66+
format: Option<String>,
67+
5168
/// Print only the X-Brin-* response headers instead of the JSON body
5269
#[arg(long)]
5370
headers: bool,
@@ -64,7 +81,21 @@ async fn main() -> anyhow::Result<()> {
6481
artifact,
6582
details,
6683
webhook,
84+
tolerance,
85+
refresh,
86+
mode,
87+
format,
6788
headers,
68-
} => commands::check::run(&client, &artifact, details, webhook.as_deref(), headers).await,
89+
} => {
90+
let opts = CheckOptions {
91+
details,
92+
webhook: webhook.as_deref(),
93+
tolerance: tolerance.as_deref(),
94+
refresh,
95+
mode: mode.as_deref(),
96+
format: format.as_deref(),
97+
};
98+
commands::check::run(&client, &artifact, &opts, headers).await
99+
}
69100
}
70101
}

0 commit comments

Comments
 (0)