modelos-secure 0.1.0 → 0.1.2
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 +2 -0
- package/assets/login-BoqCvsTW.js +1 -0
- package/assets/{login-B21lxf7i.js → login-JPkrnvZC.js} +109 -20
- package/cli.js +7 -5
- package/package.json +1 -1
- package/assets/login-CVPBOTvx.js +0 -1
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-JPkrnvZC.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;
|
|
@@ -369,13 +426,27 @@ function refusalKind(answer) {
|
|
|
369
426
|
if (!json && status < 500 && (status < 300 || page)) return "not-the-app";
|
|
370
427
|
return null;
|
|
371
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* IAP's own words for a refusal, when it gave them as plain text, such as "Invalid bearer token.
|
|
431
|
+
* Audience doesn't match the allowlisted oauth clients for this application." A page gives none.
|
|
432
|
+
*/
|
|
433
|
+
function iapReason(headers, text) {
|
|
434
|
+
if ((headers.get("content-type") ?? "").includes("html")) return void 0;
|
|
435
|
+
const line = text.trim().split("\n")[0]?.trim() ?? "";
|
|
436
|
+
if (!(headers.get("x-goog-iap-generated-response") === "true" || line.startsWith("Invalid IAP credentials")) || !line || line.startsWith("<")) return void 0;
|
|
437
|
+
const words = line.replace(/^Invalid IAP credentials:\s*/i, "").slice(0, 240).trim();
|
|
438
|
+
if (!words) return void 0;
|
|
439
|
+
return /[.!?]$/.test(words) ? words : `${words}.`;
|
|
440
|
+
}
|
|
372
441
|
/** What happened, why, and what to do, in the voice of the CLI's other failures. */
|
|
373
|
-
function refusalMessage(kind, credential, url, ci = Boolean(process.env.CI)) {
|
|
442
|
+
function refusalMessage(kind, credential, url, ci = Boolean(process.env.CI), detail = {}) {
|
|
374
443
|
if (kind === "not-the-app") return `${url} answered, but it is not ModelOS Secure. Check the address against the login command on the tokens page.`;
|
|
375
444
|
if (kind === "no-sign-in") return ci ? "This address needs a Google sign-in, and none was found. In CI, set MODELOS_SECURE_CLIENT_ID with gcloud signed in as a service account, or set MODELOS_SECURE_IAP_TOKEN." : "This address needs a Google sign-in, and this machine has none yet. Run modelos-secure login with the command from the tokens page.";
|
|
376
445
|
if (kind === "not-allowed") return credential.email ? `You are signed in to Google as ${credential.email}, but that account is not allowed through to ModelOS Secure. Ask whoever runs ModelOS Secure to give it access.` : "This Google account is not allowed through to ModelOS Secure. Ask whoever runs ModelOS Secure to give it access.";
|
|
377
|
-
|
|
378
|
-
if (credential.source === "
|
|
446
|
+
const said = detail.reason ? `It said: ${detail.reason}` : detail.redirected ? "It sent the request to its sign-in page instead." : "";
|
|
447
|
+
if (credential.source === "env") return `Google refused the token in MODELOS_SECURE_IAP_TOKEN: it has expired, or it was made for another client ID. Make a new one for the client ID on the tokens page.${said ? ` ${said}` : ""}`;
|
|
448
|
+
if (credential.source === "gcloud") return `Google refused the identity token from gcloud: it was made for another client ID, or the account is not a service account. Check MODELOS_SECURE_CLIENT_ID against the tokens page.${said ? ` ${said}` : ""}`;
|
|
449
|
+
if (said) return `Google did not let your sign-in through to ModelOS Secure. ${said} Run modelos-secure login again. If it says the same, send this message to whoever runs ModelOS Secure.`;
|
|
379
450
|
return "Google refused your sign-in: it has expired or was revoked. Run modelos-secure login again.";
|
|
380
451
|
}
|
|
381
452
|
// src/api.ts
|
|
@@ -390,14 +461,14 @@ async function readConnection() {
|
|
|
390
461
|
const url = process.env.MODELOS_SECURE_URL ?? saved?.url;
|
|
391
462
|
const token = process.env.MODELOS_SECURE_TOKEN ?? saved?.token;
|
|
392
463
|
if (!url || !token) return null;
|
|
393
|
-
const
|
|
394
|
-
|
|
464
|
+
const same = Boolean(saved?.url && trim(saved.url) === trim(url));
|
|
465
|
+
const google = same ? saved?.google : void 0;
|
|
466
|
+
const tlsName = process.env.MODELOS_SECURE_TLS_NAME ?? (same ? saved?.tlsName : void 0);
|
|
467
|
+
return {
|
|
395
468
|
url: trim(url),
|
|
396
469
|
token,
|
|
397
|
-
google
|
|
398
|
-
|
|
399
|
-
url: trim(url),
|
|
400
|
-
token
|
|
470
|
+
...google ? { google } : {},
|
|
471
|
+
...tlsName ? { tlsName } : {}
|
|
401
472
|
};
|
|
402
473
|
}
|
|
403
474
|
/** The same connection at another address. The Google sign-in goes only to the same origin. */
|
|
@@ -431,7 +502,8 @@ async function saveRenewed(url, google) {
|
|
|
431
502
|
if (saved?.url && saved.token && trim(saved.url) === url && saved.google?.clientId === google.clientId) await saveConnection({
|
|
432
503
|
url: saved.url,
|
|
433
504
|
token: saved.token,
|
|
434
|
-
google
|
|
505
|
+
google,
|
|
506
|
+
...saved.tlsName ? { tlsName: saved.tlsName } : {}
|
|
435
507
|
});
|
|
436
508
|
}
|
|
437
509
|
var ApiError = class extends Error {
|
|
@@ -468,16 +540,26 @@ function apiClient(connection, options = {}) {
|
|
|
468
540
|
};
|
|
469
541
|
const call = async (method, path, body, signal) => {
|
|
470
542
|
const sent = await credential();
|
|
471
|
-
const
|
|
543
|
+
const url = `${connection.url}/api/v1${path}`;
|
|
544
|
+
const headers = {
|
|
545
|
+
authorization: `Bearer ${sent.token ?? connection.token}`,
|
|
546
|
+
"x-modelos-secure-token": connection.token,
|
|
547
|
+
...body ? { "content-type": body.type } : {}
|
|
548
|
+
};
|
|
549
|
+
const response = await (connection.tlsName ? fetchCheckingName(connection.tlsName)(url, {
|
|
550
|
+
method,
|
|
551
|
+
headers,
|
|
552
|
+
body: body?.text,
|
|
553
|
+
signal
|
|
554
|
+
}) : fetch(url, {
|
|
472
555
|
method,
|
|
473
556
|
redirect: "manual",
|
|
474
|
-
headers
|
|
475
|
-
authorization: `Bearer ${connection.token}`,
|
|
476
|
-
...sent.token ? { "proxy-authorization": `Bearer ${sent.token}` } : {},
|
|
477
|
-
...body ? { "content-type": body.type } : {}
|
|
478
|
-
},
|
|
557
|
+
headers,
|
|
479
558
|
body: body?.text,
|
|
480
559
|
signal
|
|
560
|
+
})).catch((error) => {
|
|
561
|
+
if (signal?.aborted) throw error;
|
|
562
|
+
throw new ApiError(networkMessage(error, connection.url, connection.tlsName), 0);
|
|
481
563
|
});
|
|
482
564
|
const text = await response.text();
|
|
483
565
|
let parsed;
|
|
@@ -492,7 +574,10 @@ function apiClient(connection, options = {}) {
|
|
|
492
574
|
body: parsed,
|
|
493
575
|
sent: Boolean(sent.token)
|
|
494
576
|
});
|
|
495
|
-
if (kind) throw new IapRefusal(kind, response.status, refusalMessage(kind, sent, connection.url
|
|
577
|
+
if (kind) throw new IapRefusal(kind, response.status, refusalMessage(kind, sent, connection.url, void 0, {
|
|
578
|
+
reason: iapReason(response.headers, text),
|
|
579
|
+
redirected: response.status >= 300 && response.status < 400
|
|
580
|
+
}));
|
|
496
581
|
if (!response.ok) {
|
|
497
582
|
const error = typeof parsed === "object" && parsed !== null ? parsed.error : void 0;
|
|
498
583
|
throw new ApiError(typeof error === "string" ? error : `The server answered ${response.status.toString()}.`, response.status);
|
|
@@ -524,25 +609,29 @@ function parseLoginLine(line) {
|
|
|
524
609
|
return {
|
|
525
610
|
url: after("url"),
|
|
526
611
|
clientId: after("client-id"),
|
|
527
|
-
clientSecret: after("client-secret")
|
|
612
|
+
clientSecret: after("client-secret"),
|
|
613
|
+
tlsName: after("tls-name")
|
|
528
614
|
};
|
|
529
615
|
}
|
|
530
616
|
async function loginCommand(given = {}) {
|
|
531
617
|
let url = given.url ?? process.env.MODELOS_SECURE_URL;
|
|
532
|
-
let { clientId, clientSecret } = given;
|
|
618
|
+
let { clientId, clientSecret, tlsName } = given;
|
|
533
619
|
if (!url) {
|
|
534
620
|
const pasted = parseLoginLine(await askText("Where is ModelOS Secure? Paste the login command from the tokens page, or type the address:"));
|
|
535
621
|
url = pasted.url;
|
|
536
622
|
clientId ??= pasted.clientId;
|
|
537
623
|
clientSecret ??= pasted.clientSecret;
|
|
624
|
+
tlsName ??= pasted.tlsName;
|
|
538
625
|
}
|
|
626
|
+
tlsName ??= process.env.MODELOS_SECURE_TLS_NAME;
|
|
539
627
|
if (!url) throw new Error("A login needs the address. Copy the login command from the tokens page.");
|
|
540
628
|
if (clientId && !clientSecret) throw new Error("The client ID needs its client secret. Copy the whole login command from the tokens page.");
|
|
541
629
|
const token = process.env.MODELOS_SECURE_TOKEN ?? await askText("Paste a workspace token from the tokens page:", true);
|
|
542
630
|
if (!token) throw new Error("A login needs a workspace token. Create one on the tokens page.");
|
|
543
631
|
const connection = {
|
|
544
632
|
url: url.replace(/\/$/, ""),
|
|
545
|
-
token
|
|
633
|
+
token,
|
|
634
|
+
...tlsName ? { tlsName } : {}
|
|
546
635
|
};
|
|
547
636
|
if (clientId && clientSecret) connection.google = await signInWithGoogle(clientId, clientSecret);
|
|
548
637
|
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-
|
|
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-JPkrnvZC.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-
|
|
19488
|
+
const { loginCommand } = await import("./assets/login-BoqCvsTW.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
package/assets/login-CVPBOTvx.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { t as loginCommand } from "./login-B21lxf7i.js";
|