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.
- package/bin/atris.js +3 -0
- package/commands/security-review.js +165 -17
- package/commands/sentinel-bench.js +172 -0
- package/commands/sentinel-fix.js +243 -0
- package/lib/security-audit.js +529 -13
- package/lib/sentinel-agents.js +75 -41
- package/lib/sentinel-bench.js +556 -0
- package/lib/sentinel-cloud.js +12 -6
- package/lib/sentinel-events.js +112 -1
- package/lib/sentinel-fix.js +777 -0
- package/lib/sentinel-guard-install.js +10 -4
- package/lib/sentinel-guard.js +168 -89
- package/lib/sentinel-improve.js +823 -0
- package/lib/sentinel-rules.js +108 -0
- package/lib/sentinel-rules.json +237 -0
- package/lib/sentinel-watch.js +12 -6
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/lib/sentinel-watch.js
CHANGED
|
@@ -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
|
|
346
|
-
const
|
|
347
|
-
const
|
|
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
|
-
|
|
362
|
-
return
|
|
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.
|
|
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",
|