atris 3.29.0 → 3.30.1

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.
@@ -31,6 +31,8 @@ const {
31
31
  applyBaseline,
32
32
  shouldFail,
33
33
  scoreFindings,
34
+ recordRun,
35
+ buildLanding,
34
36
  } = require('../lib/security-scan');
35
37
 
36
38
  const ICON = { critical: '✗', high: '✗', medium: '!', low: '·', privacy: '✗', secret: '✗', pii: '!' };
@@ -46,6 +48,8 @@ function parseArgs(argv) {
46
48
  noBaseline: false,
47
49
  deep: false,
48
50
  md: false,
51
+ land: false,
52
+ noRecord: false,
49
53
  baseline: DEFAULT_BASELINE,
50
54
  paths: [],
51
55
  };
@@ -60,6 +64,8 @@ function parseArgs(argv) {
60
64
  else if (arg === '--no-baseline') opts.noBaseline = true;
61
65
  else if (arg === '--deep') opts.deep = true;
62
66
  else if (arg === '--md' || arg === '--markdown') opts.md = true;
67
+ else if (arg === '--land' || arg === '--landing') opts.land = true;
68
+ else if (arg === '--no-record') opts.noRecord = true;
63
69
  else if (arg === '--baseline') {
64
70
  if (!argv[i + 1] || argv[i + 1].startsWith('-')) throw new Error('--baseline requires a path');
65
71
  opts.baseline = argv[++i];
@@ -144,6 +150,13 @@ function securityReviewCommand(argv = []) {
144
150
  const failing = failingCount(findings, threshold);
145
151
  const ok = !shouldFail(findings, threshold);
146
152
 
153
+ // Flight recorder + landing: the loop appends one row per run; the landing
154
+ // compares this run to the last one. buildLanding must read the ledger BEFORE
155
+ // recordRun appends this run.
156
+ const scanResult = { findings, counts, scanned: raw.scanned, suppressed: result.suppressed };
157
+ const landing = buildLanding(root, scanResult, { failOn: threshold });
158
+ if (!opts.noRecord) recordRun(root, scanResult, { failOn: threshold });
159
+
147
160
  if (opts.json) {
148
161
  console.log(JSON.stringify({
149
162
  ok,
@@ -155,6 +168,7 @@ function securityReviewCommand(argv = []) {
155
168
  score: scoreFindings(findings),
156
169
  score_all: scoreFindings(allFindings),
157
170
  fail_threshold: threshold,
171
+ landing,
158
172
  findings,
159
173
  deep_review: opts.deep ? deepReviewPayload({ findings, counts, scanned: raw.scanned, suppressed: result.suppressed }) : undefined,
160
174
  generated_for: 'soc2-evidence',
@@ -178,6 +192,11 @@ function securityReviewCommand(argv = []) {
178
192
  return ok ? 0 : 1;
179
193
  }
180
194
 
195
+ if (opts.land) {
196
+ console.log(renderLanding(landing, threshold));
197
+ return ok ? 0 : 1;
198
+ }
199
+
181
200
  if (!opts.quiet) {
182
201
  console.log('\n ◉ atris security review');
183
202
  if (!findings.length) {
@@ -205,6 +224,49 @@ function securityReviewCommand(argv = []) {
205
224
  return ok ? 0 : 1;
206
225
  }
207
226
 
227
+ // The landing: short, true, decision-ready. What you read after the overnight
228
+ // loop — the opposite of a finding dump.
229
+ function renderLanding(landing, threshold) {
230
+ const date = new Date().toISOString().slice(0, 10);
231
+ const L = ['', ` ✈ security landing — ${date}`, ''];
232
+ if (landing.cleared) {
233
+ L.push(` CLEARED TO SHIP no unresolved findings at the ${threshold.toUpperCase()} line`);
234
+ } else {
235
+ const n = landing.open.length;
236
+ L.push(` HOLD ${n} finding${n === 1 ? '' : 's'} need a decision before ship`);
237
+ }
238
+ L.push('');
239
+ if (landing.hadPrevRun) {
240
+ L.push(' since last run:');
241
+ L.push(` fixed ${landing.fixed}`);
242
+ L.push(` new ${landing.appeared}`);
243
+ L.push(` accepted ${landing.accepted} (known-safe, in baseline)`);
244
+ } else {
245
+ L.push(` first run: ${landing.open.length} open · ${landing.accepted} accepted (baseline)`);
246
+ }
247
+ L.push('');
248
+ if (landing.open.length) {
249
+ L.push(' needs you:');
250
+ for (const f of landing.open.slice(0, 10)) {
251
+ L.push(` ${f.sev.toUpperCase().padEnd(8)} ${f.file}${f.line ? ':' + f.line : ''} — ${f.why}`);
252
+ }
253
+ if (landing.open.length > 10) L.push(` … and ${landing.open.length - 10} more`);
254
+ } else {
255
+ L.push(' needs you: nothing');
256
+ }
257
+ L.push('');
258
+ if (landing.trend.length > 1) {
259
+ const a = landing.trend[0], b = landing.trend[landing.trend.length - 1];
260
+ const da = a.critical + a.high, db = b.critical + b.high;
261
+ const dir = db < da ? 'improving' : db > da ? 'worsening' : 'steady';
262
+ L.push(` posture: critical ${a.critical}→${b.critical}, high ${a.high}→${b.high} over ${landing.trend.length} runs (${dir})`);
263
+ } else {
264
+ L.push(` posture: ${landing.runs} run${landing.runs === 1 ? '' : 's'} recorded · scanned ${landing.scanned} files`);
265
+ }
266
+ L.push('');
267
+ return L.join('\n');
268
+ }
269
+
208
270
  function printRules() {
209
271
  console.log('\n atris security-review — deterministic rules:\n');
210
272
  for (const sev of SEVERITIES) {
@@ -343,6 +405,8 @@ function printHelp() {
343
405
  atris security-review --json machine output / SOC 2 evidence artifact
344
406
  atris security-review --md markdown evidence report
345
407
  atris security-review --deep prompt a stronger model with framework + evidence
408
+ atris security-review --land the landing: cleared-to-ship or hold, what changed,
409
+ what needs you, the trend (read this after the loop)
346
410
  atris security-review --update-baseline
347
411
  accept current findings in .security-review.baseline.json
348
412
  atris security-review --no-baseline