modelos-secure 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,6 +29,8 @@ npx modelos-secure login --url <address> --client-id <client ID> --client-secret
29
29
 
30
30
  Paste the workspace token when it asks. The token is never typed on the command line. Your browser then opens for you to sign in with Google. The address, the token and the Google sign-in are kept in `~/.modelos-secure/credentials.json`, readable only by you, and the sign-in is renewed as it runs out. Without `--client-id`, as on a local stack, there is no Google sign-in.
31
31
 
32
+ When the app is reached at an IP address and its certificate is issued for a name, the login command also has `--tls-name` and that name. The CLI then connects to the address and checks the certificate against the name: its chain, its dates and the name are all still checked. `MODELOS_SECURE_TLS_NAME` does the same for one run. When something cannot be reached, the CLI says why in plain words.
33
+
32
34
  `MODELOS_SECURE_URL` and `MODELOS_SECURE_TOKEN` take the place of the saved address and token. `modelos-secure doctor` says who is signed in to Google, and why a request was turned away.
33
35
 
34
36
  ## In CI
@@ -0,0 +1 @@
1
+ export { t as loginCommand } from "./login-WwE-Gunj.js";
@@ -5,6 +5,8 @@ import { execFile, spawn } from "node:child_process";
5
5
  import { createHash, randomBytes } from "node:crypto";
6
6
  import { createServer } from "node:http";
7
7
  import { emitKeypressEvents } from "node:readline";
8
+ import { request } from "node:https";
9
+ import { checkServerIdentity } from "node:tls";
8
10
  // src/ui.ts
9
11
  var color = process.stdout.isTTY && !process.env.NO_COLOR;
10
12
  var unicode = process.env.TERM !== "dumb" && !process.env.MODELOS_SECURE_ASCII;
@@ -276,6 +278,61 @@ function gcloudIdentityToken(audience) {
276
278
  });
277
279
  });
278
280
  }
281
+ // src/tls.ts
282
+ /** A fetch that connects to the URL's host and checks its certificate against `name`. */
283
+ function fetchCheckingName(name) {
284
+ return (url, init = {}) => new Promise((resolve, reject) => {
285
+ const outgoing = request(new URL(url), {
286
+ method: init.method ?? "GET",
287
+ headers: init.headers,
288
+ servername: name,
289
+ checkServerIdentity: (_host, certificate) => checkServerIdentity(name, certificate),
290
+ signal: init.signal
291
+ }, (incoming) => {
292
+ const chunks = [];
293
+ incoming.on("data", (chunk) => chunks.push(chunk));
294
+ incoming.on("error", reject);
295
+ incoming.on("end", () => {
296
+ const headers = new Headers();
297
+ for (const [key, value] of Object.entries(incoming.headers)) if (value !== void 0) headers.set(key, Array.isArray(value) ? value.join(", ") : value);
298
+ const status = incoming.statusCode ?? 502;
299
+ resolve(new Response(status === 204 || status === 304 ? null : Buffer.concat(chunks), {
300
+ status,
301
+ headers
302
+ }));
303
+ });
304
+ });
305
+ outgoing.on("error", reject);
306
+ if (init.body !== void 0) outgoing.write(init.body);
307
+ outgoing.end();
308
+ });
309
+ }
310
+ /** What went wrong reaching an address, in plain words, from the error Node gave. */
311
+ function networkMessage(error, url, tlsName) {
312
+ const cause = error?.cause;
313
+ const code = (typeof cause?.code === "string" ? cause.code : void 0) ?? error?.code;
314
+ let host = url;
315
+ try {
316
+ host = new URL(url).hostname;
317
+ } catch {}
318
+ switch (code) {
319
+ case "ERR_TLS_CERT_ALTNAME_INVALID": return tlsName ? `The certificate at ${host} is not issued for ${tlsName}. Copy the login command from the tokens page again.` : `The certificate at ${host} is not issued for that address. If it is issued for a name, such as one in your company's domain, log in again with --tls-name and that name.`;
320
+ case "DEPTH_ZERO_SELF_SIGNED_CERT":
321
+ case "SELF_SIGNED_CERT_IN_CHAIN":
322
+ case "UNABLE_TO_VERIFY_LEAF_SIGNATURE":
323
+ case "UNABLE_TO_GET_ISSUER_CERT_LOCALLY": return `This machine does not trust the certificate at ${host}. If your company adds its own certificates to this machine, set NODE_OPTIONS=--use-system-ca and try again.`;
324
+ case "CERT_HAS_EXPIRED": return `The certificate at ${host} has expired. Tell whoever runs that address.`;
325
+ case "ENOTFOUND":
326
+ case "EAI_AGAIN": return `${host} cannot be found from this machine. Check the address.`;
327
+ case "ECONNREFUSED":
328
+ case "ECONNRESET":
329
+ case "ETIMEDOUT":
330
+ case "EHOSTUNREACH":
331
+ case "ENETUNREACH":
332
+ case "UND_ERR_CONNECT_TIMEOUT": return `${host} cannot be reached from this machine. It may only be reachable on the company network.`;
333
+ default: return `The request to ${host} failed${code ? ` (${code})` : ""}.`;
334
+ }
335
+ }
279
336
  // src/iap.ts
280
337
  /**
281
338
  * The Google credential for one connection, in this order: MODELOS_SECURE_IAP_TOKEN as it is;
@@ -390,14 +447,14 @@ async function readConnection() {
390
447
  const url = process.env.MODELOS_SECURE_URL ?? saved?.url;
391
448
  const token = process.env.MODELOS_SECURE_TOKEN ?? saved?.token;
392
449
  if (!url || !token) return null;
393
- const google = saved?.google && saved.url && trim(saved.url) === trim(url) ? saved.google : void 0;
394
- return google ? {
450
+ const same = Boolean(saved?.url && trim(saved.url) === trim(url));
451
+ const google = same ? saved?.google : void 0;
452
+ const tlsName = process.env.MODELOS_SECURE_TLS_NAME ?? (same ? saved?.tlsName : void 0);
453
+ return {
395
454
  url: trim(url),
396
455
  token,
397
- google
398
- } : {
399
- url: trim(url),
400
- token
456
+ ...google ? { google } : {},
457
+ ...tlsName ? { tlsName } : {}
401
458
  };
402
459
  }
403
460
  /** The same connection at another address. The Google sign-in goes only to the same origin. */
@@ -431,7 +488,8 @@ async function saveRenewed(url, google) {
431
488
  if (saved?.url && saved.token && trim(saved.url) === url && saved.google?.clientId === google.clientId) await saveConnection({
432
489
  url: saved.url,
433
490
  token: saved.token,
434
- google
491
+ google,
492
+ ...saved.tlsName ? { tlsName: saved.tlsName } : {}
435
493
  });
436
494
  }
437
495
  var ApiError = class extends Error {
@@ -468,16 +526,26 @@ function apiClient(connection, options = {}) {
468
526
  };
469
527
  const call = async (method, path, body, signal) => {
470
528
  const sent = await credential();
471
- const response = await fetch(`${connection.url}/api/v1${path}`, {
529
+ const url = `${connection.url}/api/v1${path}`;
530
+ const headers = {
531
+ authorization: `Bearer ${connection.token}`,
532
+ ...sent.token ? { "proxy-authorization": `Bearer ${sent.token}` } : {},
533
+ ...body ? { "content-type": body.type } : {}
534
+ };
535
+ const response = await (connection.tlsName ? fetchCheckingName(connection.tlsName)(url, {
536
+ method,
537
+ headers,
538
+ body: body?.text,
539
+ signal
540
+ }) : fetch(url, {
472
541
  method,
473
542
  redirect: "manual",
474
- headers: {
475
- authorization: `Bearer ${connection.token}`,
476
- ...sent.token ? { "proxy-authorization": `Bearer ${sent.token}` } : {},
477
- ...body ? { "content-type": body.type } : {}
478
- },
543
+ headers,
479
544
  body: body?.text,
480
545
  signal
546
+ })).catch((error) => {
547
+ if (signal?.aborted) throw error;
548
+ throw new ApiError(networkMessage(error, connection.url, connection.tlsName), 0);
481
549
  });
482
550
  const text = await response.text();
483
551
  let parsed;
@@ -524,25 +592,29 @@ function parseLoginLine(line) {
524
592
  return {
525
593
  url: after("url"),
526
594
  clientId: after("client-id"),
527
- clientSecret: after("client-secret")
595
+ clientSecret: after("client-secret"),
596
+ tlsName: after("tls-name")
528
597
  };
529
598
  }
530
599
  async function loginCommand(given = {}) {
531
600
  let url = given.url ?? process.env.MODELOS_SECURE_URL;
532
- let { clientId, clientSecret } = given;
601
+ let { clientId, clientSecret, tlsName } = given;
533
602
  if (!url) {
534
603
  const pasted = parseLoginLine(await askText("Where is ModelOS Secure? Paste the login command from the tokens page, or type the address:"));
535
604
  url = pasted.url;
536
605
  clientId ??= pasted.clientId;
537
606
  clientSecret ??= pasted.clientSecret;
607
+ tlsName ??= pasted.tlsName;
538
608
  }
609
+ tlsName ??= process.env.MODELOS_SECURE_TLS_NAME;
539
610
  if (!url) throw new Error("A login needs the address. Copy the login command from the tokens page.");
540
611
  if (clientId && !clientSecret) throw new Error("The client ID needs its client secret. Copy the whole login command from the tokens page.");
541
612
  const token = process.env.MODELOS_SECURE_TOKEN ?? await askText("Paste a workspace token from the tokens page:", true);
542
613
  if (!token) throw new Error("A login needs a workspace token. Create one on the tokens page.");
543
614
  const connection = {
544
615
  url: url.replace(/\/$/, ""),
545
- token
616
+ token,
617
+ ...tlsName ? { tlsName } : {}
546
618
  };
547
619
  if (clientId && clientSecret) connection.google = await signInWithGoogle(clientId, clientSecret);
548
620
  const me = await apiClient(connection).get("/whoami");
package/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { _ as step, a as apiClient, c as Interrupted, d as blue, f as bold, g as say, h as red, i as IapRefusal, l as ask, m as grey, o as atAddress, p as dash, r as ApiError$1, s as readConnection, t as loginCommand, u as askText } from "./assets/login-B21lxf7i.js";
2
+ import { _ as step, a as apiClient, c as Interrupted, d as blue, f as bold, g as say, h as red, i as IapRefusal, l as ask, m as grey, o as atAddress, p as dash, r as ApiError$1, s as readConnection, t as loginCommand, u as askText } from "./assets/login-WwE-Gunj.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { existsSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
@@ -19485,7 +19485,7 @@ async function scanCommand(dir, options) {
19485
19485
  label: "Enter a token now",
19486
19486
  recommended: true,
19487
19487
  apply: async () => {
19488
- const { loginCommand } = await import("./assets/login-CVPBOTvx.js");
19488
+ const { loginCommand } = await import("./assets/login-BcHFyX6z.js");
19489
19489
  await loginCommand();
19490
19490
  const fresh = await readConnection();
19491
19491
  api = fresh ? apiClient(fresh) : null;
@@ -19678,7 +19678,7 @@ function flag(name) {
19678
19678
  const index = rest.indexOf(`--${name}`);
19679
19679
  return index >= 0 ? rest[index + 1] : void 0;
19680
19680
  }
19681
- var positional = rest.filter((arg, i) => !arg.startsWith("--") && !rest[i - 1]?.match(/^--(format|out|system|instances|against|budget|url|client-id|client-secret)$/));
19681
+ var positional = rest.filter((arg, i) => !arg.startsWith("--") && !rest[i - 1]?.match(/^--(format|out|system|instances|against|budget|url|client-id|client-secret|tls-name)$/));
19682
19682
  async function connection() {
19683
19683
  const found = await readConnection();
19684
19684
  if (!found) throw new Error("No workspace token found. Run modelos-secure login, or set MODELOS_SECURE_TOKEN.");
@@ -19687,7 +19687,8 @@ async function connection() {
19687
19687
  var USAGE = `modelos-secure: test an AI system for Security, Safety and Quality.
19688
19688
 
19689
19689
  login Saves a workspace token, and signs in with Google when the
19690
- address needs it. --url, --client-id, --client-secret.
19690
+ address needs it. --url, --client-id, --client-secret,
19691
+ --tls-name.
19691
19692
  scan . Connects this repository. --offline, --explain, --yes.
19692
19693
  review Starts a review of this system. --quick, --ci, --budget.
19693
19694
  replay F-031 Replays one finding.
@@ -19703,7 +19704,8 @@ try {
19703
19704
  if (command === "login") await loginCommand({
19704
19705
  url: flag("url"),
19705
19706
  clientId: flag("client-id"),
19706
- clientSecret: flag("client-secret")
19707
+ clientSecret: flag("client-secret"),
19708
+ tlsName: flag("tls-name")
19707
19709
  });
19708
19710
  else if (command === "scan") await scanCommand(resolve(positional[0] ?? "."), {
19709
19711
  offline: has("offline"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelos-secure",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Tests AI systems for Security, Safety and Quality. One command connects a system from its code.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -1 +0,0 @@
1
- export { t as loginCommand } from "./login-B21lxf7i.js";