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
package/lib/sentinel-events.js
CHANGED
|
@@ -112,8 +112,11 @@ function cleanRepo(name) {
|
|
|
112
112
|
return REPO_SHAPE.test(s) && !strictSecret(s) ? s : 'project';
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// The project name for any Sentinel upload: lib/sentinel-guard.js
|
|
116
|
+
// projectName (git remote repo name, else folder name) passed through the
|
|
117
|
+
// secret check above.
|
|
115
118
|
function repoName(root) {
|
|
116
|
-
return cleanRepo(
|
|
119
|
+
return cleanRepo(require('./sentinel-guard').projectName(root || process.cwd()));
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
function makeEvent({ kind, repo, severity = 'info', title, detail = '', status, fingerprint: fp, occurredAt = new Date() }) {
|
|
@@ -249,6 +252,112 @@ async function reportFindings(root, kind, openEvents, { token, base, env = proce
|
|
|
249
252
|
return { ...res, sent: res.ok ? plan.events.length + extra.length : 0, resolved: plan.resolved.length };
|
|
250
253
|
}
|
|
251
254
|
|
|
255
|
+
// ---------------------------------------------------------------- review fixes
|
|
256
|
+
|
|
257
|
+
// Review findings a later review closed (status fixed in a review upload).
|
|
258
|
+
// The overview counts a fix when a stored open event comes back resolved,
|
|
259
|
+
// so a finding never sent open goes first as open (dated when its review
|
|
260
|
+
// found it) and then as resolved in a second call. One call would store it
|
|
261
|
+
// resolved and the fix would never count. Local state remembers what was
|
|
262
|
+
// sent: uploading the same review again never counts a fix twice.
|
|
263
|
+
// Open review findings are not sent as events: the overview already counts
|
|
264
|
+
// them from the review itself.
|
|
265
|
+
//
|
|
266
|
+
// The record is keyed by the shared project name, not the checkout path, so
|
|
267
|
+
// every copy of one project on this machine shares it. Each entry is keyed
|
|
268
|
+
// by the closure id: the project name plus the finding's fingerprint, the
|
|
269
|
+
// same pair the server stores the event under.
|
|
270
|
+
//
|
|
271
|
+
// Accepted limit: the server has no idempotency field yet. Two machines
|
|
272
|
+
// (or two uploads racing on one) can each send the open then resolved pair
|
|
273
|
+
// for one finding. Both land on the same stored event and it stays one
|
|
274
|
+
// row. Its fixed time moves to the later send.
|
|
275
|
+
const REVIEW_STATE_KIND = 'review';
|
|
276
|
+
const FIXED_DETAIL = 'A later review found the line that closes this problem.';
|
|
277
|
+
|
|
278
|
+
function reviewStatePath(repo, env) {
|
|
279
|
+
return path.join(sentinelDir(env), 'state', `review-${hash(cleanRepo(repo)).slice(0, 16)}.json`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function closureId(repo, fp) {
|
|
283
|
+
return `${cleanRepo(repo)}/${fp}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function readStateFile(file) {
|
|
287
|
+
try {
|
|
288
|
+
const s = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
289
|
+
return { open: (s && s.open) || {}, resolved: (s && s.resolved) || {} };
|
|
290
|
+
} catch { return { open: {}, resolved: {} }; }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Earlier builds keyed this record by checkout path; fold that one in so a
|
|
294
|
+
// fix already sent from this checkout is not sent again.
|
|
295
|
+
function readReviewState(root, repo, env) {
|
|
296
|
+
const state = readStateFile(reviewStatePath(repo, env));
|
|
297
|
+
if (root) {
|
|
298
|
+
const legacy = readStateFile(statePath(root, REVIEW_STATE_KIND, env));
|
|
299
|
+
for (const part of ['open', 'resolved']) {
|
|
300
|
+
for (const [fp, at] of Object.entries(legacy[part])) {
|
|
301
|
+
const id = fp.includes('/') ? fp : closureId(repo, fp);
|
|
302
|
+
if (!state[part][id]) state[part][id] = at;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return state;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function writeReviewState(repo, state, env) {
|
|
310
|
+
const file = reviewStatePath(repo, env);
|
|
311
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
312
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
313
|
+
fs.writeFileSync(tmp, `${JSON.stringify({ repo: cleanRepo(repo), kind: REVIEW_STATE_KIND, ...state, updated_at: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
314
|
+
fs.renameSync(tmp, file);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function reviewFixEvent(body, f, status, at) {
|
|
318
|
+
return makeEvent({
|
|
319
|
+
kind: 'threat',
|
|
320
|
+
repo: body.repo,
|
|
321
|
+
severity: f.severity === 'informational' ? 'info' : f.severity,
|
|
322
|
+
title: f.title,
|
|
323
|
+
detail: status === 'resolved' ? FIXED_DETAIL : '',
|
|
324
|
+
status,
|
|
325
|
+
fingerprint: f.id,
|
|
326
|
+
occurredAt: at,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function reportReviewFixes(root, body, { token, base, env = process.env, foundAt = null, now = new Date() } = {}) {
|
|
331
|
+
const repo = cleanRepo(body && body.repo);
|
|
332
|
+
const findings = ((body && body.findings) || []).filter((f) => f && /^f-[0-9a-f]{12}$/.test(String(f.id || '')));
|
|
333
|
+
const fixed = findings.filter((f) => f.status === 'fixed');
|
|
334
|
+
const state = readReviewState(root, repo, env);
|
|
335
|
+
const id = (f) => closureId(repo, f.id);
|
|
336
|
+
// A finding open again can be fixed again later and count again.
|
|
337
|
+
const back = findings.filter((f) => f.status === 'open' && state.resolved[id(f)]);
|
|
338
|
+
if (back.length) {
|
|
339
|
+
for (const f of back) delete state.resolved[id(f)];
|
|
340
|
+
try { writeReviewState(repo, state, env); } catch {}
|
|
341
|
+
}
|
|
342
|
+
if (!fixed.length) return { ok: true, sent: 0, fixed: 0 };
|
|
343
|
+
const todo = fixed.filter((f) => !state.resolved[id(f)]);
|
|
344
|
+
if (!todo.length) return { ok: true, sent: 0, fixed: 0, already: fixed.length };
|
|
345
|
+
if (!token) return { ok: false, reason: 'not logged in', sent: 0, fixed: 0 };
|
|
346
|
+
const found = foundAt && !Number.isNaN(Date.parse(foundAt)) ? new Date(foundAt) : now;
|
|
347
|
+
const opening = todo.filter((f) => !state.open[id(f)]);
|
|
348
|
+
if (opening.length) {
|
|
349
|
+
const res = await postEvents(opening.map((f) => reviewFixEvent(body, f, 'open', found)), { token, base });
|
|
350
|
+
if (!res.ok) return { ...res, sent: 0, fixed: 0 };
|
|
351
|
+
for (const f of opening) state.open[id(f)] = found.toISOString();
|
|
352
|
+
try { writeReviewState(repo, state, env); } catch {}
|
|
353
|
+
}
|
|
354
|
+
const res = await postEvents(todo.map((f) => reviewFixEvent(body, f, 'resolved', now)), { token, base });
|
|
355
|
+
if (!res.ok) return { ...res, sent: opening.length, fixed: 0 };
|
|
356
|
+
for (const f of todo) { state.resolved[id(f)] = now.toISOString(); delete state.open[id(f)]; }
|
|
357
|
+
try { writeReviewState(repo, state, env); } catch {}
|
|
358
|
+
return { ok: true, sent: opening.length + todo.length, fixed: todo.length };
|
|
359
|
+
}
|
|
360
|
+
|
|
252
361
|
// ---------------------------------------------------------------- queue
|
|
253
362
|
|
|
254
363
|
function queuePath(env = process.env) {
|
|
@@ -307,10 +416,12 @@ module.exports = {
|
|
|
307
416
|
fillPlain,
|
|
308
417
|
slotOk,
|
|
309
418
|
repoName,
|
|
419
|
+
cleanRepo,
|
|
310
420
|
makeEvent,
|
|
311
421
|
postEvents,
|
|
312
422
|
chunks,
|
|
313
423
|
reportFindings,
|
|
424
|
+
reportReviewFixes,
|
|
314
425
|
readState,
|
|
315
426
|
statePath,
|
|
316
427
|
queuePath,
|