Repository: denoland/deno
Pull request: feat(unstable): support authenticated socket.dev audit reports

`SOCKET_API_KEY="test-api-key-12345" deno audit --socket`

Changed files:
filename: cli/tools/pm/audit.rs
patch: @@ -12,6 +12,8 @@ use deno_core::serde_json;
 use deno_npm::resolution::NpmResolutionSnapshot;
 use deno_resolver::npmrc::npm_registry_url;
 use eszip::v2::Url;
+use http::header::HeaderName;
+use http::header::HeaderValue;
 use serde::Deserialize;
 use serde::Serialize;
 
@@ -561,25 +563,84 @@ mod socket_dev {
     npm_resolution_snapshot: &NpmResolutionSnapshot,
     client: HttpClient,
   ) -> Result<(), AnyError> {
-    let socket_dev_url = std::env::var("SOCKET_DEV_URL")
-      .ok()
-      .unwrap_or_else(|| "https://firewall-api.socket.dev/".to_string());
-
     let purls = npm_resolution_snapshot
       .all_packages_for_every_system()
       .map(|package| {
         format!("pkg:npm/{}@{}", package.id.nv.name, package.id.nv.version)
       })
       .collect::<Vec<_>>();
 
+    let api_key = std::env::var("SOCKET_API_KEY").ok();
+
+    let mut purl_responses = if let Some(api_key) = api_key {
+      call_authenticated_api(&client, &purls, &api_key).await?
+    } else {
+      call_unauthenticated_api(&client, &purls).await?
+    };
+
+    purl_responses.sort_by_cached_key(|r| r.name.to_string());
+
+    print_firewall_report(&purl_responses);
+
+    Ok(())
+  }
+
+  async fn call_authenticated_api(
+    client: &HttpClient,
+    purls: &[String],
+    api_key: &str,
+  ) -> Result<Vec<FirewallResponse>, AnyError> {
+    let socket_dev_url =
+      std::env::var("SOCKET_DEV_URL").ok().unwrap_or_else(|| {
+        "https://api.socket.dev/v0/purl?actions=error,warn".to_string()
+      });
+    let url = Url::parse(&socket_dev_url).unwrap();
+
+    let body = serde_json::json!({
+      "components": purls.iter().map(|purl| {
+        serde_json::json!({ "purl": purl })
+      }).collect::<Vec<_>>()
+    });
+
+    let auth_value = HeaderValue::from_str(&format!("Bearer {}", api_key))
+      .context("Failed to create Authorization header")?;
+
+    let request = client
+      .post_json(url, &body)?
+      .header(HeaderName::from_static("authorization"), auth_value);
+
+    let response = request.send().boxed_local().await?;
+    let text = http_util::body_to_string(response).await?;
+
+    // Response is nJSON
+    let responses = text
+      .lines()
+      .filter(|line| !line.trim().is_empty())
+      .map(|line| {
+        serde_json::from_str::<FirewallResponse>(line)
+          .context("Failed to parse Socket.dev response")
+      })
+      .collect::<Result<Vec<_>, _>>()?;
+
+    Ok(responses)
+  }
+
+  async fn call_unauthenticated_api(
+    client: &HttpClient,
+    purls: &[String],
+  ) -> Result<Vec<FirewallResponse>, AnyError> {
+    let socket_dev_url = std::env::var("SOCKET_DEV_URL")
+      .ok()
+      .unwrap_or_else(|| "https://firewall-api.socket.dev/".to_string());
+
     let futures = purls
-      .into_iter()
+      .iter()
       .map(|purl| {
         let url = Url::parse(&format!(
           "{}purl/{}",
           socket_dev_url,
           percent_encoding::utf8_percent_encode(
-            &purl,
+            purl,
             percent_encoding::NON_ALPHANUMERIC
           )
         ))
@@ -592,7 +653,8 @@ mod socket_dev {
       .buffer_unordered(20)
       .collect::<Vec<_>>()
       .await;
-    let mut purl_responses = purl_results
+
+    let responses = purl_results
       .into_iter()
       .filter_map(|result| match result {
         Ok(a) => Some(a),
@@ -607,17 +669,13 @@ mod socket_dev {
         response
       })
       .collect::<Vec<_>>();
-    purl_responses.sort_by_cached_key(|r| r.name.to_string());
-
-    print_firewall_report(&purl_responses);
 
-    Ok(())
+    Ok(responses)
   }
 
   fn print_firewall_report(responses: &[FirewallResponse]) {
     let stdout = &mut std::io::stdout();
 
-    // Separator
     _ = writeln!(stdout);
     _ = writeln!(stdout, "{}", colors::bold("Socket.dev firewall report"));
     _ = writeln!(stdout);
@@ -638,7 +696,6 @@ mod socket_dev {
 
       _ = writeln!(stdout, "╭ pkg:npm/{}@{}", response.name, response.version);
 
-      // Print scores if available
       if let Some(score) = &response.score {
         _ = writeln!(
           stdout,
@@ -672,7 +729,7 @@ mod socket_dev {
         );
       }
 
-      // Count alerts by severity
+      // critical and high are counted as one for display.
       let mut critical_count = 0;
       let mut medium_count = 0;
       let mut low_count = 0;

filename: tests/specs/audit/package_json_only/__test__.jsonc
patch: @@ -14,6 +14,14 @@
       "args": "audit --socket",
       "output": "audit_socket.out",
       "exitCode": 1
+    },
+    {
+      "args": "audit --socket",
+      "output": "audit_socket_authenticated.out",
+      "exitCode": 1,
+      "envs": {
+        "SOCKET_API_KEY": "test-api-key-12345"
+      }
     }
   ]
 }

filename: tests/specs/audit/package_json_only/audit_socket_authenticated.out
patch: @@ -0,0 +1,50 @@
+╭ @denotest/with-vuln1 is susceptible to prototype pollution
+│ Severity:   high
+│ Package:    @edenotest/with-vuln1
+│ Vulnerable: <1.1.0
+│ Patched:    >=1.1.0
+│ Path:       @denotest/with-vuln1
+│ Info:       https://example.com/vuln/101010
+╰ Actions:    install @denotest/with-vuln1@1.1.0
+
+╭ @denotest/with-vuln2 can steal crypto keys
+│ Severity:   critical
+│ Package:    @edenotest/with-vuln2
+│ Vulnerable: <2.0.0
+│ Patched:    >=2.0.0
+│ Path:       @denotest/using-vuln>@denotest/with-vuln2
+│ Info:       https://example.com/vuln/202020
+│ Actions:    install @denotest/with-vuln2@2.0.0 (major upgrade)
+╰             review @denotest/with-vuln2
+
+Found 2 vulnerabilities
+Severity: 0 low, 0 moderate, 1 high, 1 critical
+
+Socket.dev firewall report
+
+╭ pkg:npm/@denotest/using-vuln@1.0.0
+│ Supply Chain Risk:   100
+│ Maintenance:          78
+│ Quality:              94
+│ Vulnerabilities:     100
+│ License:             100
+╰ Alerts (1/0/0):      [critical] malware
+
+╭ pkg:npm/@denotest/with-vuln1@1.0.0
+│ Supply Chain Risk:   100
+│ Maintenance:          78
+│ Quality:              94
+│ Vulnerabilities:     100
+│ License:             100
+╰ Alerts (1/0/0):      [critical] malware
+
+╭ pkg:npm/@denotest/with-vuln2@1.5.0
+│ Supply Chain Risk:   100
+│ Maintenance:          78
+│ Quality:              94
+│ Vulnerabilities:     100
+│ License:             100
+╰ Alerts (1/0/0):      [critical] malware
+
+Found 3 alerts across 3 packages
+Severity: 0 low, 0 medium, 0 high, 3 critical

filename: tests/util/server/src/servers/socket_dev.rs
patch: @@ -64,6 +64,12 @@ async fn socket_dev_handler(
   req: Request<hyper::body::Incoming>,
 ) -> Result<Response<UnsyncBoxBody<Bytes, Infallible>>, anyhow::Error> {
   let path = req.uri().path();
+  let method = req.method();
+
+  // Handle authenticated mode: POST /v0/purl
+  if method == hyper::Method::POST {
+    return handle_authenticated_request(req).await;
+  }
 
   // Expected format: /purl/{percent_encoded_purl}
   // where purl is like: pkg:npm/package-name@version
@@ -141,3 +147,71 @@ async fn socket_dev_handler(
       .body(string_body(&response_body))?,
   )
 }
+
+async fn handle_authenticated_request(
+  req: Request<hyper::body::Incoming>,
+) -> Result<Response<UnsyncBoxBody<Bytes, Infallible>>, anyhow::Error> {
+  use http_body_util::BodyExt;
+
+  // Read the request body
+  let body_bytes = req.collect().await?.to_bytes();
+  let body_str = String::from_utf8(body_bytes.to_vec())?;
+
+  // Parse the JSON body
+  let body_json: serde_json::Value = serde_json::from_str(&body_str)?;
+  let components = body_json["components"]
+    .as_array()
+    .ok_or_else(|| anyhow::anyhow!("Missing components array"))?;
+
+  // Build newline-delimited JSON response
+  let mut responses = Vec::new();
+
+  for component in components {
+    let purl = component["purl"]
+      .as_str()
+      .ok_or_else(|| anyhow::anyhow!("Missing purl field"))?;
+
+    // Parse the purl format: pkg:npm/package-name@version
+    if !purl.starts_with("pkg:npm/") {
+      continue;
+    }
+
+    let package_part = &purl[8..]; // Skip "pkg:npm/"
+    let parts: Vec<&str> = package_part.rsplitn(2, '@').collect();
+    if parts.len() != 2 {
+      continue;
+    }
+
+    let version = parts[0];
+    let name = parts[1];
+
+    let response_json = json!({
+      "id": "81646",
+      "name": name,
+      "version": version,
+      "score": {
+        "license": 1.0,
+        "maintenance": 0.78,
+        "overall": 0.78,
+        "quality": 0.94,
+        "supplyChain": 1.0,
+        "vulnerability": 1.0
+      },
+      "alerts": [
+        { "type": "malware", "action": "error", "severity": "critical", "category": "supplyChainRisk" }
+      ]
+    });
+
+    responses.push(response_json.to_string());
+  }
+
+  // Join with newlines for newline-delimited JSON
+  let response_body = responses.join("\n");
+
+  Ok(
+    Response::builder()
+      .status(StatusCode::OK)
+      .header("Content-Type", "application/json")
+      .body(string_body(&response_body))?,
+  )
+}