modelos-secure 0.3.2 → 0.3.3

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.
Files changed (2) hide show
  1. package/cli.js +70 -19
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -3,9 +3,9 @@ import { _ as lastFinishedStep, a as apiClient, b as status, c as Interrupted, d
3
3
  import { createRequire } from "node:module";
4
4
  import { existsSync } from "node:fs";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
- import { basename, delimiter, dirname, extname, join, relative, resolve, sep } from "node:path";
6
+ import { basename, delimiter, dirname, extname, join, posix, relative, resolve, sep } from "node:path";
7
7
  import { parseArgs } from "node:util";
8
- import { access, appendFile, constants, glob, mkdir, readFile, readdir, realpath, rename, stat, writeFile } from "node:fs/promises";
8
+ import { access, appendFile, constants, glob, lstat, mkdir, readFile, readdir, realpath, rename, stat, writeFile } from "node:fs/promises";
9
9
  import { tmpdir } from "node:os";
10
10
  import { spawn } from "node:child_process";
11
11
  import { createHash, randomUUID } from "node:crypto";
@@ -23,7 +23,7 @@ if (existsSync(resolve(here, "wasm"))) {
23
23
  }
24
24
  // package.json
25
25
  var name = "modelos-secure";
26
- var version$1 = "0.3.2";
26
+ var version$1 = "0.3.3";
27
27
  var type = "module";
28
28
  var bin = { "modelos-secure": "dist/cli.js" };
29
29
  var exports$1 = { "./hook": "./dist/hook/hook.mjs" };
@@ -335,7 +335,17 @@ var SKIPPED = /* @__PURE__ */ new Set([
335
335
  "e2e",
336
336
  "coverage",
337
337
  ".turbo",
338
- ".cache"
338
+ ".cache",
339
+ ".idea",
340
+ ".vscode",
341
+ ".vs",
342
+ ".terraform",
343
+ ".ruff_cache",
344
+ ".nox",
345
+ ".gradle",
346
+ ".yarn",
347
+ ".svelte-kit",
348
+ ".angular"
339
349
  ]);
340
350
  var MAX_FILE_BYTES = 1048576;
341
351
  var DOCUMENT = /* @__PURE__ */ new Set([
@@ -5621,6 +5631,30 @@ function docOf(node, file) {
5621
5631
  // ../../packages/scan/src/start-candidates.ts
5622
5632
  /** Words a README command starts with that start an app, not install or test one. */
5623
5633
  var STARTS = /^(?:\$\s*)?((?:uvicorn|gunicorn|hypercorn|fastapi (?:dev|run)|flask (?:--app \S+ )?run|streamlit run|chainlit run|gradio|python3? (?:-m )?\S+\.py|python3? -m \S+|(?:npm|pnpm|yarn|bun) (?:run )?(?:dev|start|serve)|node \S+\.(?:m?js|ts)|npx tsx \S+|docker compose up|docker-compose up)\b.*)$/;
5634
+ /** The words of an instruction in its list form, or null when it is in the shell form. */
5635
+ function listForm(body) {
5636
+ try {
5637
+ const parts = JSON.parse(body);
5638
+ return Array.isArray(parts) ? parts.map(String) : null;
5639
+ } catch {
5640
+ return null;
5641
+ }
5642
+ }
5643
+ /**
5644
+ * What a Dockerfile runs, as Docker reads it: the last stage's entrypoint with its command as the
5645
+ * arguments, unless the entrypoint is in the shell form, which ignores them. A health check's own
5646
+ * CMD is not it. Only arguments, starting with a dash, go to a program in the base image, which is
5647
+ * not here to name, so they are no command at all.
5648
+ */
5649
+ function dockerCommand(dockerfile) {
5650
+ const lines = dockerfile.replace(/\\\r?\n/g, " ").split(/\r?\n/).map((l) => l.trim());
5651
+ const stage = lines.slice(lines.findLastIndex((l) => /^FROM\s/i.test(l)) + 1);
5652
+ const last = (word) => stage.findLast((l) => new RegExp(`^${word}\\s`, "i").test(l))?.slice(word.length).trim();
5653
+ const entrypoint = last("ENTRYPOINT");
5654
+ if (entrypoint && !listForm(entrypoint)) return entrypoint;
5655
+ const full = [entrypoint, last("CMD")].filter((body) => body !== void 0).map((body) => listForm(body)?.join(" ") ?? body).join(" ");
5656
+ return full && !full.startsWith("-") ? full : null;
5657
+ }
5624
5658
  async function startCandidates(look, project) {
5625
5659
  const files = new Set(project.files.map((f) => f.path));
5626
5660
  const read = (file) => readFile(join(look.root, file), "utf8").catch(() => null);
@@ -5648,19 +5682,12 @@ async function startCandidates(look, project) {
5648
5682
  from: `the main block at ${file.path}:${(main + 1).toString()}`
5649
5683
  });
5650
5684
  }
5651
- const cmd = (await read("Dockerfile"))?.split("\n").reverse().find((l) => /^\s*(CMD|ENTRYPOINT)\s/i.test(l));
5652
- if (cmd) {
5653
- const body = cmd.replace(/^\s*(CMD|ENTRYPOINT)\s+/i, "").trim();
5654
- let command = body;
5655
- try {
5656
- const parts = JSON.parse(body);
5657
- if (Array.isArray(parts)) command = parts.map(String).join(" ");
5658
- } catch {}
5659
- if (command) found.push({
5660
- command,
5661
- from: "the Dockerfile's command"
5662
- });
5663
- }
5685
+ const dockerfile = await read("Dockerfile");
5686
+ const command = dockerfile ? dockerCommand(dockerfile) : null;
5687
+ if (command) found.push({
5688
+ command,
5689
+ from: "the Dockerfile's command"
5690
+ });
5664
5691
  for (const name of [
5665
5692
  "README.md",
5666
5693
  "readme.md",
@@ -6395,15 +6422,36 @@ var LIMITS = {
6395
6422
  maxFileBytes: 1e6
6396
6423
  };
6397
6424
  var CODE_TOOLS = ["grep", "read"];
6398
- /** Every file in the repository the tools could see, skipping what the scan never walks. */
6425
+ var NOT_OPENED = "the scan never opens that folder";
6426
+ /** Whether the scan opens a folder at all: no part of its path is one it never walks. */
6427
+ var opensFolder = (dir) => !dir.split("/").some((part) => SKIPPED.has(part.toLowerCase()));
6428
+ /**
6429
+ * Every file in the repository the tools could see, and so what --explain counts. All of it by hand
6430
+ * rather than by glob, whose `*` leaves out names starting with a dot, which is where settings files
6431
+ * are. A link is not a file here: it could lead outside the repository. ADR 0093.
6432
+ */
6399
6433
  async function repositoryFiles(root, pattern = "**/*", most = 5e3) {
6400
6434
  const found = [];
6435
+ if (pattern === "**/*") {
6436
+ const walk = async (dir) => {
6437
+ const entries = await readdir(join(root, dir), { withFileTypes: true }).catch(() => []);
6438
+ for (const entry of entries) {
6439
+ if (found.length >= most) return;
6440
+ const path = posix.join(dir, entry.name);
6441
+ if (entry.isFile()) found.push(path);
6442
+ else if (entry.isDirectory() && opensFolder(entry.name)) await walk(path);
6443
+ }
6444
+ };
6445
+ await walk("");
6446
+ return found.sort();
6447
+ }
6401
6448
  for await (const entry of glob(pattern, {
6402
6449
  cwd: root,
6403
6450
  exclude: (name) => SKIPPED.has(String(name).split(/[\\/]/).at(-1) ?? "")
6404
6451
  })) {
6405
6452
  const path = String(entry).split("\\").join("/");
6406
- if ((await stat(join(root, path)).catch(() => null))?.isFile()) found.push(path);
6453
+ if (!opensFolder(posix.dirname(path))) continue;
6454
+ if ((await lstat(join(root, path)).catch(() => null))?.isFile()) found.push(path);
6407
6455
  if (found.length >= most) break;
6408
6456
  }
6409
6457
  return found.sort();
@@ -6419,6 +6467,7 @@ function createTools(input) {
6419
6467
  const dir = String(args.dir ?? ".");
6420
6468
  const inRepo = await insideRepository(root, dir);
6421
6469
  if (inRepo === null) return refused(dir, "it is not a folder in this repository");
6470
+ if (!opensFolder(inRepo)) return refused(inRepo, NOT_OPENED);
6422
6471
  const entries = await readdir(join(root, inRepo), { withFileTypes: true }).catch(() => null);
6423
6472
  if (!entries) return refused(dir, "it is not a folder in this repository");
6424
6473
  return {
@@ -6463,6 +6512,7 @@ function createTools(input) {
6463
6512
  if (inRepo === null) return refused(path, "it is not a file in this repository");
6464
6513
  const why = neverRead(inRepo);
6465
6514
  if (why) return refused(inRepo, why);
6515
+ if (!opensFolder(posix.dirname(inRepo))) return refused(inRepo, NOT_OPENED);
6466
6516
  if ((await stat(join(root, inRepo))).size > LIMITS.maxFileBytes) return refused(inRepo, "it is over 1 MB");
6467
6517
  const all = (await readFile(join(root, inRepo), "utf8")).split(/\r?\n/);
6468
6518
  const from = Math.max(1, Number(args.from ?? (args.to === void 0 ? 1 : args.to - LIMITS.readLines + 1)));
@@ -22103,6 +22153,7 @@ async function scanCommand(dir, options) {
22103
22153
  say(`\nNever sent, whatever is asked (${neverSent.length.toString()}):`);
22104
22154
  for (const n of neverSent) say(` ${n.path}, because ${n.why}`);
22105
22155
  if (neverSent.length === 0) say(" nothing here matches");
22156
+ say("\nThe scan never opens folders such as .git, node_modules and tests, so nothing in them is listed or read.");
22106
22157
  say(`\nWhat was actually sent on earlier runs is in .modelos-secure/sent.jsonl.`);
22107
22158
  return;
22108
22159
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelos-secure",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
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": {