modelos-secure 0.1.2 → 0.3.0

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
@@ -6,18 +6,37 @@ Tests an AI system for Security, Safety and Quality. One command in the system's
6
6
  npx modelos-secure scan .
7
7
  ```
8
8
 
9
- It reads the code, draws the system's map and asks only what it cannot work out. Everything it decides goes into `modelos-secure.yaml`. Before it sends anything it says exactly what leaves the machine, and `--explain` prints the outline without sending it. `--offline` keeps everything on your machine.
10
-
11
- | Command | Does |
12
- | --------------------------------------------- | -------------------------------------------------------------- |
13
- | `modelos-secure login` | Saves a workspace token, and signs in with Google if needed. |
14
- | `modelos-secure scan .` | Connects this repository. `--offline`, `--explain`, `--yes`. |
15
- | `modelos-secure review` | Starts a review of this system. `--quick`, `--ci`, `--budget`. |
16
- | `modelos-secure replay F-031` | Replays one finding. |
17
- | `modelos-secure doctor` | Checks the setup and fixes what it can. |
18
- | `modelos-secure export R-1179 --format sarif` | Writes a review's findings as SARIF or JSON. |
19
-
20
- During a review, actions are held and never run. Outside a review the hooks do nothing. Requires Node 20 or newer. Python apps need Python 3.10 or newer.
9
+ It reads the code the way an engineer would, starting where the app starts, draws the system's map and asks only what it cannot work out. Everything it decides goes into `modelos-secure.yaml`.
10
+
11
+ Before it sends anything it asks once whether a model may read the code it needs, or only an outline of it, or nothing. Every answer it sends passes a filter that takes secrets out, and settings files, keys and credentials are never read. `.modelos-secure/sent.jsonl` lists what was sent, `--explain` shows the outline and what may and may not be read without sending anything, and `--offline` keeps everything on your machine.
12
+
13
+ At a terminal, `modelos-secure` on its own starts a session about the app in the folder: ask about it in your own words, and each answer cites the files and lines it rests on, checked against the code.
14
+
15
+ | Command | Does |
16
+ | --------------------------------------------- | ------------------------------------------------------------------ |
17
+ | `modelos-secure` | At a terminal, a session about the app in this folder. |
18
+ | `modelos-secure login` | Saves a workspace token, and signs in with Google if needed. |
19
+ | `modelos-secure scan .` | Connects this repository. `--offline`, `--explain`, `--yes`. |
20
+ | `modelos-secure review` | Starts a review of this system. `--quick`, `--ci`, `--budget`. |
21
+ | `modelos-secure replay F-031` | Replays one finding. |
22
+ | `modelos-secure doctor` | Checks the setup and fixes what it can. |
23
+ | `modelos-secure runner` | Serves a review's attempts from this machine, in the app's folder. |
24
+ | `modelos-secure export R-1179 --format sarif` | Writes a review's findings as SARIF or JSON. |
25
+ | `modelos-secure --version` | Prints this CLI's version. |
26
+
27
+ A review of an app connected by the scan reaches it through a runner on the machine that runs it. `modelos-secure review` starts one itself, and keeps it while the review is paused; a review started from the app needs `modelos-secure runner` running in the app's folder, and says so if none is. A start command is checked before it runs and saved only once the app has started with it.
28
+
29
+ During a review, actions are held and never run. Outside a review the hooks do nothing. Requires Node 22 or newer. Python apps need Python 3.10 or newer.
30
+
31
+ Every command says what it takes with `--help`, such as `modelos-secure scan --help`, and refuses a flag it does not take.
32
+
33
+ | Exit code | Means |
34
+ | --------- | -------------------------------------------------------------- |
35
+ | 0 | Done. |
36
+ | 1 | Something failed, or a fix check found a break. |
37
+ | 2 | The command was used wrongly. |
38
+ | 3 | There are questions only a person can answer, as `--ci` lists. |
39
+ | 130 | Stopped with Ctrl-C. What was done is kept. |
21
40
 
22
41
  ## Signing in
23
42
 
package/dist/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # modelos-secure
2
+
3
+ Tests an AI system for Security, Safety and Quality. One command in the system's repository connects it, with no code changes:
4
+
5
+ ```bash
6
+ npx modelos-secure scan .
7
+ ```
8
+
9
+ It reads the code the way an engineer would, starting where the app starts, draws the system's map and asks only what it cannot work out. Everything it decides goes into `modelos-secure.yaml`.
10
+
11
+ Before it sends anything it asks once whether a model may read the code it needs, or only an outline of it, or nothing. Every answer it sends passes a filter that takes secrets out, and settings files, keys and credentials are never read. `.modelos-secure/sent.jsonl` lists what was sent, `--explain` shows the outline and what may and may not be read without sending anything, and `--offline` keeps everything on your machine.
12
+
13
+ At a terminal, `modelos-secure` on its own starts a session about the app in the folder: ask about it in your own words, and each answer cites the files and lines it rests on, checked against the code.
14
+
15
+ | Command | Does |
16
+ | --------------------------------------------- | ------------------------------------------------------------------ |
17
+ | `modelos-secure` | At a terminal, a session about the app in this folder. |
18
+ | `modelos-secure login` | Saves a workspace token, and signs in with Google if needed. |
19
+ | `modelos-secure scan .` | Connects this repository. `--offline`, `--explain`, `--yes`. |
20
+ | `modelos-secure review` | Starts a review of this system. `--quick`, `--ci`, `--budget`. |
21
+ | `modelos-secure replay F-031` | Replays one finding. |
22
+ | `modelos-secure doctor` | Checks the setup and fixes what it can. |
23
+ | `modelos-secure runner` | Serves a review's attempts from this machine, in the app's folder. |
24
+ | `modelos-secure export R-1179 --format sarif` | Writes a review's findings as SARIF or JSON. |
25
+ | `modelos-secure --version` | Prints this CLI's version. |
26
+
27
+ A review of an app connected by the scan reaches it through a runner on the machine that runs it. `modelos-secure review` starts one itself, and keeps it while the review is paused; a review started from the app needs `modelos-secure runner` running in the app's folder, and says so if none is. A start command is checked before it runs and saved only once the app has started with it.
28
+
29
+ During a review, actions are held and never run. Outside a review the hooks do nothing. Requires Node 22 or newer. Python apps need Python 3.10 or newer.
30
+
31
+ Every command says what it takes with `--help`, such as `modelos-secure scan --help`, and refuses a flag it does not take.
32
+
33
+ | Exit code | Means |
34
+ | --------- | -------------------------------------------------------------- |
35
+ | 0 | Done. |
36
+ | 1 | Something failed, or a fix check found a break. |
37
+ | 2 | The command was used wrongly. |
38
+ | 3 | There are questions only a person can answer, as `--ci` lists. |
39
+ | 130 | Stopped with Ctrl-C. What was done is kept. |
40
+
41
+ ## Signing in
42
+
43
+ Copy the login command from the workspace tokens page in the app. It has the app's address and, when the app is behind Google sign-in, the Google client to sign in with:
44
+
45
+ ```bash
46
+ npx modelos-secure login --url <address> --client-id <client ID> --client-secret <client secret>
47
+ ```
48
+
49
+ 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.
50
+
51
+ 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.
52
+
53
+ `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.
54
+
55
+ ## In CI
56
+
57
+ CI has no browser, so it signs in to Google as a service account. The service account needs IAP-secured Web App User on the app, and the client ID must be the one on the tokens page. Set `MODELOS_SECURE_URL` and `MODELOS_SECURE_TOKEN`, then one of these:
58
+
59
+ - `MODELOS_SECURE_CLIENT_ID`, the client ID from the tokens page, with gcloud signed in as the service account. The CLI runs `gcloud auth print-identity-token --audiences=<client ID>` and runs it again as the token runs out, so it suits reviews of any length. It does not work with a person's own gcloud sign-in.
60
+ - `MODELOS_SECURE_IAP_TOKEN`, an ID token for the service account with the client ID as its audience. It is sent as it is and never renewed, and it lasts about an hour. In GitHub Actions, `google-github-actions/auth` makes one with `token_format: id_token`, `id_token_audience` set to the client ID and `id_token_include_email: true`.
@@ -0,0 +1 @@
1
+ export { t as loginCommand } from "./login-DeAJGhiN.js";
@@ -5,7 +5,7 @@ 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";
8
+ import { request as request$1 } from "node:https";
9
9
  import { checkServerIdentity } from "node:tls";
10
10
  // src/ui.ts
11
11
  var color = process.stdout.isTTY && !process.env.NO_COLOR;
@@ -17,12 +17,35 @@ var grey = (t) => paint("90", t);
17
17
  var bold = (t) => paint("1", t);
18
18
  var interactive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY) && !process.env.CI;
19
19
  var pointer = unicode ? "›" : ">";
20
- var dash = unicode ? "–" : "-";
21
20
  function say(line = "") {
22
21
  console.log(line);
23
22
  }
24
23
  var STEP_WIDTH = 13;
24
+ var lastStep = "";
25
+ /** The last step that finished, so a stop can say where it happened. */
26
+ var lastFinishedStep = () => lastStep;
27
+ /** Work to finish before the process ends on Ctrl-C, such as stopping a review it started. */
28
+ var onStop = [];
29
+ function beforeStopping(work) {
30
+ onStop.push(work);
31
+ return () => {
32
+ onStop.splice(onStop.indexOf(work), 1);
33
+ };
34
+ }
35
+ async function finishBeforeStopping() {
36
+ await Promise.race([Promise.allSettled(onStop.map((work) => work())), new Promise((resolve) => setTimeout(resolve, 5e3))]);
37
+ }
38
+ /**
39
+ * One line that changes in place while work goes on, at a terminal only: what is being read now.
40
+ * An empty line clears it. Off a terminal it writes nothing, so a log holds only finished steps.
41
+ */
42
+ function status(line) {
43
+ if (!process.stdout.isTTY) return;
44
+ const width = Math.max(20, (process.stdout.columns || 80) - 4);
45
+ process.stdout.write(`\r\u001b[2K${line ? ` ${grey(line.slice(0, width))}` : ""}`);
46
+ }
25
47
  function step(name, detail, state = "done") {
48
+ if (state === "done") lastStep = name;
26
49
  const right = state === "done" ? "done" : "";
27
50
  say(` ${bold(name.padEnd(STEP_WIDTH))}${detail}${right ? ` ${grey(right)}` : ""}`);
28
51
  }
@@ -83,15 +106,19 @@ async function ask(question, why, choices) {
83
106
  async function askText(question, secret = false) {
84
107
  process.stdout.write(` ${question} `);
85
108
  if (!interactive()) return "";
86
- return new Promise((resolve) => {
109
+ return new Promise((resolve, reject) => {
87
110
  let value = "";
88
111
  emitKeypressEvents(process.stdin);
89
112
  process.stdin.setRawMode(true);
90
113
  process.stdin.resume();
91
114
  const onKey = (text, key) => {
92
115
  if (key.ctrl && key.name === "c") {
116
+ process.stdin.off("keypress", onKey);
93
117
  process.stdin.setRawMode(false);
94
- process.exit(130);
118
+ process.stdin.pause();
119
+ process.stdout.write("\n");
120
+ reject(new Interrupted());
121
+ return;
95
122
  }
96
123
  if (key.name === "return") {
97
124
  process.stdin.off("keypress", onKey);
@@ -101,7 +128,10 @@ async function askText(question, secret = false) {
101
128
  resolve(value.trim());
102
129
  return;
103
130
  }
104
- if (key.name === "backspace") value = value.slice(0, -1);
131
+ if (key.name === "backspace") {
132
+ if (value && !secret) process.stdout.write("\b \b");
133
+ value = value.slice(0, -1);
134
+ } else if (key.name === "tab") return;
105
135
  else if (text && !key.ctrl) {
106
136
  value += text;
107
137
  if (!secret) process.stdout.write(text);
@@ -282,7 +312,7 @@ function gcloudIdentityToken(audience) {
282
312
  /** A fetch that connects to the URL's host and checks its certificate against `name`. */
283
313
  function fetchCheckingName(name) {
284
314
  return (url, init = {}) => new Promise((resolve, reject) => {
285
- const outgoing = request(new URL(url), {
315
+ const outgoing = request$1(new URL(url), {
286
316
  method: init.method ?? "GET",
287
317
  headers: init.headers,
288
318
  servername: name,
@@ -521,7 +551,15 @@ var IapRefusal = class extends ApiError {
521
551
  this.kind = kind;
522
552
  }
523
553
  };
554
+ /** Hosts a token may reach over plain http: this machine only, as in local development. */
555
+ var LOCAL_HOSTS = /* @__PURE__ */ new Set([
556
+ "localhost",
557
+ "127.0.0.1",
558
+ "[::1]"
559
+ ]);
524
560
  function apiClient(connection, options = {}) {
561
+ const target = new URL(connection.url);
562
+ if (target.protocol === "http:" && !LOCAL_HOSTS.has(target.hostname)) throw new Error(`The address ${target.origin} starts with http://, so your token would travel in clear. Use its https:// address.`);
525
563
  const proxy = proxyCredentials(connection.google, {
526
564
  saved: (google) => saveRenewed(connection.url, google),
527
565
  ...options
@@ -639,4 +677,4 @@ async function loginCommand(given = {}) {
639
677
  const who = connection.google?.email;
640
678
  say(`Saved. This machine now works in ${me.workspace}${who ? `, signed in to Google as ${who}` : ""}.`);
641
679
  }
642
- export { step as _, apiClient as a, Interrupted as c, blue as d, bold as f, say as g, red as h, IapRefusal as i, ask as l, grey as m, parseLoginLine as n, atAddress as o, dash as p, ApiError as r, readConnection as s, loginCommand as t, askText as u };
680
+ export { lastFinishedStep as _, apiClient as a, status as b, Interrupted as c, beforeStopping as d, blue as f, interactive as g, grey as h, IapRefusal as i, ask as l, finishBeforeStopping as m, parseLoginLine as n, atAddress as o, bold as p, ApiError as r, readConnection as s, loginCommand as t, askText as u, red as v, step as x, say as y };