atris 3.82.4 → 3.82.7

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.
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ // The schema for lib/sentinel-rules.json, the literal rules of the guard and
4
+ // the agent check. `atris sentinel improve` may change only that file, so
5
+ // this is the wall between a proposal and the code that judges it:
6
+ //
7
+ // known keys only, at every level. A missing or extra key is an error
8
+ // strings and lists of strings only. No functions, no objects where a
9
+ // list belongs. No `pattern` field anywhere: proposals may add or
10
+ // remove literal entries, never a regular expression
11
+ // every entry is a short literal: a host name, a program name, a secret
12
+ // word, an option name or a command substring. The guard matches them
13
+ // with plain string operations. There is no pattern to compile and
14
+ // nothing that can stall on long input
15
+ //
16
+ // validateRules() returns a list of plain problems. Empty means valid.
17
+
18
+ const MAX_LIST = 300;
19
+ const MAX_ITEM = 200;
20
+
21
+ const GUARD_KEYS = ['allow_hosts', 'net_tools', 'secret_parts', 'safe_net', 'safe_net_aliases', 'read_only', 'read_only_git', 'db_clients', 'command_blocks'];
22
+ const AGENT_KEYS = ['override_substrings', 'secret_env_names'];
23
+ const SAFE_NET_KEYS = ['flags', 'values', 'local_values', 'short_flags', 'short_values', 'items'];
24
+ const BLOCK_KEYS = ['id', 'category', 'all', 'programs'];
25
+
26
+ const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
27
+
28
+ // A proposal may never carry a regex. `pattern` is the field name the old
29
+ // shape used. It is called out by name wherever it appears.
30
+ function noPatternField(obj, where, problems) {
31
+ if (isObj(obj) && 'pattern' in obj) problems.push(`${where} has a pattern field. proposals may add literal entries only, never a regular expression`);
32
+ }
33
+
34
+ function exactKeys(obj, allowed, where, problems, optional = []) {
35
+ for (const k of Object.keys(obj)) if (!allowed.includes(k)) problems.push(`${where} has an unknown key: ${k}`);
36
+ for (const k of allowed) if (!optional.includes(k) && !(k in obj)) problems.push(`${where} is missing ${k}`);
37
+ }
38
+
39
+ function strList(v, where, problems, shape = null, maxItem = MAX_ITEM) {
40
+ if (!Array.isArray(v)) { problems.push(`${where} must be a list`); return; }
41
+ if (v.length > MAX_LIST) problems.push(`${where} has more than ${MAX_LIST} items`);
42
+ v.forEach((x, i) => {
43
+ if (typeof x !== 'string') problems.push(`${where}[${i}] must be a string`);
44
+ else if (!x.length) problems.push(`${where}[${i}] is empty`);
45
+ else if (x.length > maxItem) problems.push(`${where}[${i}] is longer than ${maxItem} characters`);
46
+ else if (shape && !shape.test(x)) problems.push(`${where}[${i}] has an odd shape: ${x.slice(0, 40)}`);
47
+ });
48
+ }
49
+
50
+ function validateRules(rules) {
51
+ const problems = [];
52
+ if (!isObj(rules)) return ['the rules file must hold one object'];
53
+ exactKeys(rules, ['version', 'guard', 'agents'], 'the rules file', problems);
54
+ if (rules.version !== 2) problems.push('version must be 2');
55
+ const g = rules.guard;
56
+ if (!isObj(g)) problems.push('guard must be an object');
57
+ else {
58
+ noPatternField(g, 'guard', problems);
59
+ exactKeys(g, GUARD_KEYS, 'guard', problems);
60
+ strList(g.allow_hosts, 'guard.allow_hosts', problems, /^[a-z0-9.:_-]{1,100}$/i);
61
+ for (const k of ['net_tools', 'read_only', 'db_clients']) strList(g[k], `guard.${k}`, problems, /^[A-Za-z0-9._+[-]{1,40}$/);
62
+ strList(g.read_only_git, 'guard.read_only_git', problems, /^[a-z-]{1,30}$/);
63
+ strList(g.secret_parts, 'guard.secret_parts', problems, /^[A-Z0-9]{1,30}$/);
64
+ if (!isObj(g.safe_net)) problems.push('guard.safe_net must be an object');
65
+ else {
66
+ for (const [prog, spec] of Object.entries(g.safe_net)) {
67
+ const where = `guard.safe_net.${prog}`;
68
+ if (!/^[a-z]{1,20}$/.test(prog)) problems.push(`${where} is not a program name`);
69
+ if (!isObj(spec)) { problems.push(`${where} must be an object`); continue; }
70
+ noPatternField(spec, where, problems);
71
+ exactKeys(spec, SAFE_NET_KEYS, where, problems);
72
+ for (const k of ['flags', 'values', 'local_values']) strList(spec[k], `${where}.${k}`, problems, /^--?[A-Za-z0-9][A-Za-z0-9-]{0,40}$/);
73
+ for (const k of ['short_flags', 'short_values']) if (typeof spec[k] !== 'string' || !/^[A-Za-z]{0,30}$/.test(spec[k])) problems.push(`${where}.${k} must be letters`);
74
+ if (typeof spec.items !== 'boolean') problems.push(`${where}.items must be true or false`);
75
+ }
76
+ }
77
+ if (!isObj(g.safe_net_aliases)) problems.push('guard.safe_net_aliases must be an object');
78
+ else for (const [alias, target] of Object.entries(g.safe_net_aliases)) {
79
+ if (!/^[a-z]{1,20}$/.test(alias) || typeof target !== 'string' || !(isObj(g.safe_net) && g.safe_net[target])) problems.push(`guard.safe_net_aliases.${alias} must name a program in safe_net`);
80
+ }
81
+ if (!Array.isArray(g.command_blocks)) problems.push('guard.command_blocks must be a list');
82
+ else {
83
+ if (g.command_blocks.length > 50) problems.push('guard.command_blocks has more than 50 items');
84
+ g.command_blocks.forEach((b, i) => {
85
+ const where = `guard.command_blocks[${i}]`;
86
+ if (!isObj(b)) { problems.push(`${where} must be an object`); return; }
87
+ noPatternField(b, where, problems);
88
+ exactKeys(b, BLOCK_KEYS, where, problems, ['programs']);
89
+ if (typeof b.id !== 'string' || !/^[a-z0-9-]{1,40}$/.test(b.id)) problems.push(`${where}.id must be short lowercase words`);
90
+ if (!['exfil', 'destroy', 'disarm'].includes(b.category)) problems.push(`${where}.category must be exfil, destroy or disarm`);
91
+ strList(b.all, `${where}.all`, problems);
92
+ if (Array.isArray(b.all) && !b.all.length) problems.push(`${where}.all needs at least one substring`);
93
+ if ('programs' in b) strList(b.programs, `${where}.programs`, problems, /^[A-Za-z0-9._+-]{1,40}$/);
94
+ });
95
+ }
96
+ }
97
+ const a = rules.agents;
98
+ if (!isObj(a)) problems.push('agents must be an object');
99
+ else {
100
+ noPatternField(a, 'agents', problems);
101
+ exactKeys(a, AGENT_KEYS, 'agents', problems);
102
+ strList(a.override_substrings, 'agents.override_substrings', problems);
103
+ strList(a.secret_env_names, 'agents.secret_env_names', problems, /^[A-Za-z_][A-Za-z0-9_.-]{0,80}$/);
104
+ }
105
+ return problems;
106
+ }
107
+
108
+ module.exports = { validateRules };
@@ -0,0 +1,237 @@
1
+ {
2
+ "version": 2,
3
+ "guard": {
4
+ "allow_hosts": [
5
+ "localhost",
6
+ "127.0.0.1",
7
+ "0.0.0.0",
8
+ "::1",
9
+ "api.github.com",
10
+ "github.com",
11
+ "uploads.github.com",
12
+ "api.openai.com",
13
+ "api.anthropic.com",
14
+ "registry.npmjs.org",
15
+ "pypi.org",
16
+ "upload.pypi.org",
17
+ "api.atris.ai",
18
+ "atris.ai",
19
+ "api.stripe.com",
20
+ "slack.com",
21
+ "api.vercel.com",
22
+ "api.render.com"
23
+ ],
24
+ "net_tools": [
25
+ "curl",
26
+ "wget",
27
+ "nc",
28
+ "ncat",
29
+ "netcat",
30
+ "socat",
31
+ "telnet",
32
+ "scp",
33
+ "sftp",
34
+ "ftp",
35
+ "rsync",
36
+ "http",
37
+ "https",
38
+ "xh",
39
+ "httpie",
40
+ "aria2c",
41
+ "lftp",
42
+ "ssh"
43
+ ],
44
+ "secret_parts": [
45
+ "KEY",
46
+ "KEYS",
47
+ "TOKEN",
48
+ "TOKENS",
49
+ "SECRET",
50
+ "SECRETS",
51
+ "PASSWORD",
52
+ "PASSWD",
53
+ "PASS",
54
+ "PWD",
55
+ "CREDENTIAL",
56
+ "CREDENTIALS",
57
+ "CREDS",
58
+ "AUTH",
59
+ "PRIVATE",
60
+ "SESSION",
61
+ "COOKIE",
62
+ "APIKEY",
63
+ "PAT",
64
+ "DSN"
65
+ ],
66
+ "safe_net": {
67
+ "curl": {
68
+ "flags": [
69
+ "-s",
70
+ "--silent",
71
+ "-S",
72
+ "--show-error",
73
+ "-f",
74
+ "--fail",
75
+ "--fail-with-body",
76
+ "--fail-early",
77
+ "-L",
78
+ "--location",
79
+ "-O",
80
+ "--remote-name",
81
+ "-i",
82
+ "--include",
83
+ "-v",
84
+ "--verbose",
85
+ "--compressed",
86
+ "-G",
87
+ "--get"
88
+ ],
89
+ "values": [
90
+ "-H",
91
+ "--header",
92
+ "-d",
93
+ "--data",
94
+ "--data-raw",
95
+ "--data-binary",
96
+ "--data-urlencode",
97
+ "--data-ascii",
98
+ "--json",
99
+ "-X",
100
+ "--request",
101
+ "-o",
102
+ "--output",
103
+ "-w",
104
+ "--write-out",
105
+ "-m",
106
+ "--max-time"
107
+ ],
108
+ "local_values": [
109
+ "-o",
110
+ "--output"
111
+ ],
112
+ "short_flags": "sSfLOivG",
113
+ "short_values": "HdXowm",
114
+ "items": false
115
+ },
116
+ "wget": {
117
+ "flags": [
118
+ "-q",
119
+ "--quiet",
120
+ "-S",
121
+ "--server-response",
122
+ "-nv",
123
+ "--no-verbose"
124
+ ],
125
+ "values": [
126
+ "-O",
127
+ "--output-document",
128
+ "--header",
129
+ "--post-data",
130
+ "--method",
131
+ "--body-data",
132
+ "-T",
133
+ "--timeout",
134
+ "-t",
135
+ "--tries"
136
+ ],
137
+ "local_values": [
138
+ "-O",
139
+ "--output-document"
140
+ ],
141
+ "short_flags": "qS",
142
+ "short_values": "OTt",
143
+ "items": false
144
+ },
145
+ "http": {
146
+ "flags": [
147
+ "-j",
148
+ "--json",
149
+ "-f",
150
+ "--form",
151
+ "-h",
152
+ "--headers",
153
+ "-b",
154
+ "--body",
155
+ "-v",
156
+ "--verbose",
157
+ "--check-status",
158
+ "--ignore-stdin"
159
+ ],
160
+ "values": [
161
+ "-p",
162
+ "--print",
163
+ "--timeout",
164
+ "-o",
165
+ "--output"
166
+ ],
167
+ "local_values": [
168
+ "-o",
169
+ "--output"
170
+ ],
171
+ "short_flags": "jfhbv",
172
+ "short_values": "po",
173
+ "items": true
174
+ }
175
+ },
176
+ "safe_net_aliases": {
177
+ "https": "http",
178
+ "xh": "http",
179
+ "httpie": "http"
180
+ },
181
+ "read_only": [
182
+ "cat",
183
+ "less",
184
+ "more",
185
+ "head",
186
+ "tail",
187
+ "grep",
188
+ "egrep",
189
+ "fgrep",
190
+ "rg",
191
+ "jq",
192
+ "ls",
193
+ "stat",
194
+ "wc",
195
+ "file",
196
+ "diff",
197
+ "echo",
198
+ "test",
199
+ "[",
200
+ "realpath",
201
+ "readlink"
202
+ ],
203
+ "read_only_git": [
204
+ "diff",
205
+ "log",
206
+ "show",
207
+ "status"
208
+ ],
209
+ "db_clients": [
210
+ "psql",
211
+ "pg_restore",
212
+ "mysql",
213
+ "mariadb",
214
+ "sqlite3",
215
+ "sqlite",
216
+ "sqlcmd",
217
+ "mongosh",
218
+ "mongo",
219
+ "redis-cli",
220
+ "clickhouse-client",
221
+ "clickhouse",
222
+ "cockroach",
223
+ "duckdb",
224
+ "supabase",
225
+ "prisma",
226
+ "turso",
227
+ "wrangler",
228
+ "neonctl",
229
+ "cqlsh"
230
+ ],
231
+ "command_blocks": []
232
+ },
233
+ "agents": {
234
+ "override_substrings": [],
235
+ "secret_env_names": []
236
+ }
237
+ }
@@ -341,25 +341,31 @@ async function runWatch(root, { base = process.env.ATRIS_OSV_BASE || OSV_BASE, r
341
341
 
342
342
  // ---------------------------------------------------------------- events
343
343
 
344
+ // The dashboard only accepts plain words, so a package name like @vitest/mocker
345
+ // or lodash.merge is written as vitest-mocker or lodash-merge.
346
+ function plainName(name) {
347
+ return String(name || '').replace(/^@/, '').replace(/[\/._]+/g, '-').replace(/[^A-Za-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
348
+ }
349
+
344
350
  function threatTitle(a) {
345
- const kind = events.slotOk('package', a.name) ? 'package' : 'module';
346
- const slots = { pkg: [kind, a.name, 'one'], fixed: ['version', a.fixed, 'a newer version'] };
347
- const subject = events.slotOk(kind, a.name) ? 'the {pkg} package you use' : 'a package you use';
351
+ const name = plainName(a.name);
352
+ const kind = events.slotOk('package', name) ? 'package' : 'module';
353
+ const slots = { pkg: [kind, name, 'one'], fixed: ['version', a.fixed, 'a newer version'] };
354
+ const subject = events.slotOk(kind, name) ? 'the {pkg} package you use' : 'a package you use';
348
355
  if (a.malicious) return events.fillPlain(`A published attack affects ${subject}. Remove it or move to a version that is not affected.`, slots);
349
356
  if (a.fixed) return events.fillPlain(`A known security hole affects ${subject}. Version {fixed} fixes it.`, slots);
350
357
  return events.fillPlain(`A known security hole affects ${subject}. No fixed version is published yet.`, slots);
351
358
  }
352
359
 
353
360
  function threatDetail(a) {
354
- const ids = a.advisories.filter((id) => events.slotOk('advisory', id)).slice(0, 5);
355
361
  const one = a.advisories.length === 1;
356
362
  const slots = { n: ['count', String(a.advisories.length), 'some'], fixed: ['version', a.fixed, 'a newer version'], current: ['version', a.version, 'the current version'] };
357
363
  const found = one ? 'Found in one published advisory against version {current}.' : 'Found in {n} published advisories against version {current}.';
358
364
  const head = a.fixed
359
365
  ? events.fillPlain(`${found} Update to version {fixed} or later and run your tests.`, slots, 1000)
360
366
  : events.fillPlain(`${found} Watch for a fix or replace the package.`, slots, 1000);
361
- const tail = ids.length ? ` Advisories: ${ids.join(' ')}.` : '';
362
- return `${head}${tail}`.slice(0, 1000);
367
+ // Advisory codes stay in the local report: mixed letter and digit codes read as keys to the dashboard's secret check.
368
+ return head.slice(0, 1000);
363
369
  }
364
370
 
365
371
  function toEvents(result, { repo, now = new Date() } = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.82.4",
3
+ "version": "3.82.7",
4
4
  "description": "you say what you want in plain words. atris builds it, checks it, and shows you proof. project memory your coding agents share across sessions.",
5
5
  "mcpName": "io.github.atrislabs/atris",
6
6
  "main": "bin/atris.js",