atris 3.27.0 → 3.29.0

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,360 @@
1
+ // atris security-review — deterministic secrets / PII / privacy scan (no LLM).
2
+ //
3
+ // "Is this workspace safe to commit, publish, or hand to an autonomous loop?"
4
+ // Answers it with facts: file:line + rule + severity. Exit 1 on a HIGH finding,
5
+ // so it drops into the autopilot/mission/CI verification gate. `--json` emits a
6
+ // SOC 2 evidence artifact. Scans git-tracked files by default (what is exposed
7
+ // is what is committed), or a path you pass.
8
+ //
9
+ // Usage:
10
+ // atris security-review scan tracked files (default)
11
+ // atris security-review src/ scan a path
12
+ // atris security-review --staged scan staged changes (pre-commit gate)
13
+ // atris security-review --json machine output for CI / the loop
14
+ // atris security-review --strict also fail on MEDIUM (PII/paths)
15
+ // atris security-review --deep model-review prompt + evidence
16
+ // atris security-review --update-baseline accept current findings
17
+ // atris security-review hook install a pre-commit gate
18
+ //
19
+ // Exit code: 0 = clean, 1 = active findings at/over the fail threshold, 2 = bad usage.
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const {
24
+ runScan,
25
+ RULES,
26
+ DEFAULT_BASELINE,
27
+ SEVERITIES,
28
+ SEVERITY_RANK,
29
+ loadBaseline,
30
+ writeBaseline,
31
+ applyBaseline,
32
+ shouldFail,
33
+ scoreFindings,
34
+ } = require('../lib/security-scan');
35
+
36
+ const ICON = { critical: '✗', high: '✗', medium: '!', low: '·', privacy: '✗', secret: '✗', pii: '!' };
37
+ const SEV_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
38
+
39
+ function parseArgs(argv) {
40
+ const opts = {
41
+ json: false,
42
+ quiet: false,
43
+ strict: false,
44
+ staged: false,
45
+ updateBaseline: false,
46
+ noBaseline: false,
47
+ deep: false,
48
+ md: false,
49
+ baseline: DEFAULT_BASELINE,
50
+ paths: [],
51
+ };
52
+ for (let i = 0; i < argv.length; i++) {
53
+ const arg = argv[i];
54
+ if (arg === 'scan') continue;
55
+ if (arg === '--json') opts.json = true;
56
+ else if (arg === '--quiet') opts.quiet = true;
57
+ else if (arg === '--strict') opts.strict = true;
58
+ else if (arg === '--staged') opts.staged = true;
59
+ else if (arg === '--update-baseline') opts.updateBaseline = true;
60
+ else if (arg === '--no-baseline') opts.noBaseline = true;
61
+ else if (arg === '--deep') opts.deep = true;
62
+ else if (arg === '--md' || arg === '--markdown') opts.md = true;
63
+ else if (arg === '--baseline') {
64
+ if (!argv[i + 1] || argv[i + 1].startsWith('-')) throw new Error('--baseline requires a path');
65
+ opts.baseline = argv[++i];
66
+ } else if (arg.startsWith('--baseline=')) {
67
+ opts.baseline = arg.slice('--baseline='.length);
68
+ } else if (arg.startsWith('-')) {
69
+ throw new Error(`unknown flag: ${arg}`);
70
+ } else {
71
+ opts.paths.push(arg);
72
+ }
73
+ }
74
+ if (opts.noBaseline && opts.updateBaseline) throw new Error('--update-baseline cannot be combined with --no-baseline');
75
+ return opts;
76
+ }
77
+
78
+ function sortFindings(findings) {
79
+ findings.sort((a, b) => (SEV_ORDER[a.sev] - SEV_ORDER[b.sev]) || a.file.localeCompare(b.file) || (a.line - b.line));
80
+ return findings;
81
+ }
82
+
83
+ function failThreshold(strict) {
84
+ return strict ? 'medium' : 'high';
85
+ }
86
+
87
+ function failingCount(findings, threshold) {
88
+ return findings.filter((f) => SEVERITY_RANK[f.sev] >= SEVERITY_RANK[threshold]).length;
89
+ }
90
+
91
+ function applyBaselineOptions(root, rawFindings, opts) {
92
+ if (opts.noBaseline) {
93
+ const active = applyBaseline(rawFindings, []);
94
+ return { ...active, baseline: { enabled: false, path: null, updated: false } };
95
+ }
96
+
97
+ let baseline = loadBaseline(root, opts.baseline);
98
+ let updated = false;
99
+ if (opts.updateBaseline) {
100
+ baseline = writeBaseline(root, rawFindings, opts.baseline);
101
+ updated = true;
102
+ }
103
+ const applied = applyBaseline(rawFindings, baseline.fingerprints);
104
+ return {
105
+ ...applied,
106
+ baseline: {
107
+ enabled: true,
108
+ path: path.relative(root, baseline.file) || opts.baseline,
109
+ updated,
110
+ fingerprints: baseline.fingerprints.length,
111
+ },
112
+ };
113
+ }
114
+
115
+ function securityReviewCommand(argv = []) {
116
+ const sub = argv[0];
117
+ if (sub === 'help' || argv.includes('-h') || argv.includes('--help')) return printHelp();
118
+ if (sub === 'rules') return printRules();
119
+ if (sub === 'hook' || sub === 'install-hook') return installHook();
120
+
121
+ const root = process.cwd();
122
+ let opts;
123
+ try {
124
+ opts = parseArgs(argv);
125
+ } catch (e) {
126
+ console.error(`security-review: ${e.message}`);
127
+ return 2;
128
+ }
129
+
130
+ let raw;
131
+ let result;
132
+ try {
133
+ raw = runScan({ root, paths: opts.paths, staged: opts.staged });
134
+ result = applyBaselineOptions(root, raw.findings, opts);
135
+ } catch (e) {
136
+ console.error(`security-review: ${e.message}`);
137
+ return 2;
138
+ }
139
+
140
+ const findings = sortFindings(result.findings);
141
+ const allFindings = sortFindings(result.allFindings);
142
+ const counts = result.counts;
143
+ const threshold = failThreshold(opts.strict);
144
+ const failing = failingCount(findings, threshold);
145
+ const ok = !shouldFail(findings, threshold);
146
+
147
+ if (opts.json) {
148
+ console.log(JSON.stringify({
149
+ ok,
150
+ scanned: raw.scanned,
151
+ counts,
152
+ counts_all: result.countsAll,
153
+ suppressed: result.suppressed,
154
+ baseline: result.baseline,
155
+ score: scoreFindings(findings),
156
+ score_all: scoreFindings(allFindings),
157
+ fail_threshold: threshold,
158
+ findings,
159
+ deep_review: opts.deep ? deepReviewPayload({ findings, counts, scanned: raw.scanned, suppressed: result.suppressed }) : undefined,
160
+ generated_for: 'soc2-evidence',
161
+ }, null, 2));
162
+ return ok ? 0 : 1;
163
+ }
164
+
165
+ if (opts.md) {
166
+ console.log(renderMarkdownReport({
167
+ findings, allFindings, counts, countsAll: result.countsAll, scanned: raw.scanned,
168
+ threshold, failing, baseline: result.baseline, suppressed: result.suppressed,
169
+ includeDeep: opts.deep,
170
+ }));
171
+ return ok ? 0 : 1;
172
+ }
173
+
174
+ if (opts.deep) {
175
+ console.log(renderDeepReview({
176
+ findings, counts, scanned: raw.scanned, threshold, suppressed: result.suppressed, baseline: result.baseline,
177
+ }));
178
+ return ok ? 0 : 1;
179
+ }
180
+
181
+ if (!opts.quiet) {
182
+ console.log('\n ◉ atris security review');
183
+ if (!findings.length) {
184
+ console.log(`\n ✓ clean — no active secrets, PII, or sensitive files in ${raw.scanned} tracked file${raw.scanned === 1 ? '' : 's'}`);
185
+ if (result.suppressed) console.log(` ${result.suppressed} accepted finding${result.suppressed === 1 ? '' : 's'} suppressed by ${result.baseline.path}`);
186
+ console.log('');
187
+ return 0;
188
+ }
189
+ console.log('');
190
+ const w = Math.max(...findings.map((f) => `${f.file}${f.line ? ':' + f.line : ''}`.length));
191
+ for (const f of findings) {
192
+ const loc = `${f.file}${f.line ? ':' + f.line : ''}`.padEnd(w);
193
+ console.log(` ${ICON[f.sev] || '·'} ${f.sev.toUpperCase().padEnd(6)} ${loc} ${f.rule.padEnd(22)} ${f.why}`);
194
+ }
195
+ }
196
+ console.log(`\n ${counts.critical || 0} critical · ${counts.high || 0} high · ${counts.medium || 0} medium · ${counts.low || 0} low across ${raw.scanned} file${raw.scanned === 1 ? '' : 's'}`);
197
+ if (result.suppressed) console.log(` ${result.suppressed} accepted finding${result.suppressed === 1 ? '' : 's'} suppressed by ${result.baseline.path}`);
198
+ if (result.baseline.updated) console.log(` baseline updated: ${result.baseline.path} (${result.baseline.fingerprints} fingerprint${result.baseline.fingerprints === 1 ? '' : 's'})`);
199
+ if (failing) {
200
+ console.log(` ${failing} finding${failing === 1 ? '' : 's'} at/over the ${threshold.toUpperCase()} threshold · exit 1`);
201
+ console.log(' fix or suppress (trailing `atris-allow-secret`), then re-run.\n');
202
+ } else {
203
+ console.log(` no findings at the ${threshold.toUpperCase()} threshold · exit 0\n`);
204
+ }
205
+ return ok ? 0 : 1;
206
+ }
207
+
208
+ function printRules() {
209
+ console.log('\n atris security-review — deterministic rules:\n');
210
+ for (const sev of SEVERITIES) {
211
+ for (const r of RULES.filter((rule) => rule.sev === sev)) {
212
+ console.log(` ${r.sev.toUpperCase().padEnd(8)} ${r.cat.padEnd(8)} ${r.id.padEnd(22)} ${r.why}`);
213
+ }
214
+ }
215
+ console.log(`\n ${RULES.length} rules + tracked-sensitive-file check. Suppress a line with a trailing \`atris-allow-secret\`.\n`);
216
+ return 0;
217
+ }
218
+
219
+ const DEEP_DIMENSIONS = [
220
+ ['secrets & keys', 'Look for real keys, tokens, private keys, credential files, and places where logs or docs could expose them.'],
221
+ ['who-can-do-what', 'Check whether users, agents, missions, and local commands can only do the actions they should be allowed to do.'],
222
+ ['untrusted input to code/shell/path/web requests', 'Trace user-controlled values into eval, new Function, child_process, file paths, URLs, redirects, and fetch/request calls.'],
223
+ ['data exposure', 'Check logs, responses, reports, task state, prompts, and errors for personal data, secrets, or private workspace paths.'],
224
+ ['dependencies', 'Check package and script changes for risky install hooks, unpinned tools, vendored code, or unexpected network execution.'],
225
+ ['crypto & randomness', 'Check token generation, signing, hashing, random IDs, and compare logic for weak or predictable behavior.'],
226
+ ['config & defaults', 'Check debug flags, CORS, open ports, default credentials, unsafe local paths, and flags that bypass safety gates.'],
227
+ ];
228
+
229
+ function evidenceLines(findings, limit = 80) {
230
+ if (!findings.length) return ['- No deterministic findings after baseline suppression.'];
231
+ return findings.slice(0, limit).map((f) => {
232
+ const loc = `${f.file}${f.line ? ':' + f.line : ''}`;
233
+ return `- ${f.sev.toUpperCase()} ${loc} ${f.rule}: ${f.why}`;
234
+ });
235
+ }
236
+
237
+ function deepReviewPayload({ findings, counts, scanned, suppressed }) {
238
+ return {
239
+ instruction: 'Answer each dimension with PASS or CONCERN. If CONCERN, cite file:line and the concrete risk. Do not speculate beyond the code.',
240
+ dimensions: DEEP_DIMENSIONS.map(([name, check]) => ({ name, check })),
241
+ evidence: {
242
+ scanned,
243
+ counts,
244
+ suppressed,
245
+ findings,
246
+ },
247
+ };
248
+ }
249
+
250
+ function renderDeepReview({ findings, counts, scanned, threshold, suppressed, baseline }) {
251
+ const lines = [
252
+ '# Atris Deep Security Review',
253
+ '',
254
+ 'Use this as a second-pass review prompt for a capable model.',
255
+ 'Answer every dimension with PASS or CONCERN, then give the file:line evidence.',
256
+ 'Do not invent issues. If the code does not prove the issue, mark PASS or say what proof is missing.',
257
+ '',
258
+ 'Deterministic evidence:',
259
+ `- scanned files: ${scanned}`,
260
+ `- active counts: ${counts.critical || 0} critical, ${counts.high || 0} high, ${counts.medium || 0} medium, ${counts.low || 0} low`,
261
+ `- fail threshold: ${threshold}`,
262
+ `- suppressed by baseline: ${suppressed}`,
263
+ `- baseline: ${baseline.enabled ? baseline.path : 'off'}`,
264
+ '',
265
+ 'Findings:',
266
+ ...evidenceLines(findings),
267
+ '',
268
+ 'Review dimensions:',
269
+ ];
270
+ for (const [name, check] of DEEP_DIMENSIONS) {
271
+ lines.push('', `## ${name}`, check, 'Answer: PASS or CONCERN', 'Specific evidence:');
272
+ }
273
+ return `${lines.join('\n')}\n`;
274
+ }
275
+
276
+ function renderMarkdownReport({ findings, allFindings, counts, countsAll, scanned, threshold, failing, baseline, suppressed, includeDeep }) {
277
+ const status = failing ? 'FAIL' : 'PASS';
278
+ const lines = [
279
+ '# Atris Security Review',
280
+ '',
281
+ `Status: ${status}`,
282
+ `Fail threshold: ${threshold}`,
283
+ `Scanned files: ${scanned}`,
284
+ `Baseline: ${baseline.enabled ? baseline.path : 'off'}`,
285
+ `Suppressed: ${suppressed}`,
286
+ '',
287
+ '## Active Counts',
288
+ '',
289
+ `- critical: ${counts.critical || 0}`,
290
+ `- high: ${counts.high || 0}`,
291
+ `- medium: ${counts.medium || 0}`,
292
+ `- low: ${counts.low || 0}`,
293
+ `- score: ${scoreFindings(findings)}`,
294
+ '',
295
+ '## All Counts',
296
+ '',
297
+ `- critical: ${countsAll.critical || 0}`,
298
+ `- high: ${countsAll.high || 0}`,
299
+ `- medium: ${countsAll.medium || 0}`,
300
+ `- low: ${countsAll.low || 0}`,
301
+ `- score: ${scoreFindings(allFindings)}`,
302
+ '',
303
+ '## Findings',
304
+ '',
305
+ ...evidenceLines(findings),
306
+ ];
307
+ if (includeDeep) {
308
+ lines.push('', '## Deep Review Prompt', '', renderDeepReview({
309
+ findings, counts, scanned, threshold, suppressed, baseline,
310
+ }).trim());
311
+ }
312
+ return `${lines.join('\n')}\n`;
313
+ }
314
+
315
+ function installHook() {
316
+ const root = process.cwd();
317
+ const hookDir = path.join(root, '.git', 'hooks');
318
+ try {
319
+ fs.mkdirSync(hookDir, { recursive: true });
320
+ const hookPath = path.join(hookDir, 'pre-commit');
321
+ const marker = '# atris security gate';
322
+ let content = '';
323
+ try { content = fs.readFileSync(hookPath, 'utf8'); } catch {}
324
+ if (content.includes(marker)) { console.log(`\n already installed: ${path.relative(root, hookPath)}\n`); return 0; }
325
+ if (!content) content = '#!/bin/sh\n';
326
+ if (!content.endsWith('\n')) content += '\n';
327
+ content += `\n${marker}\nif command -v atris >/dev/null 2>&1; then atris security-review --staged --quiet || exit 1; fi\n`;
328
+ fs.writeFileSync(hookPath, content);
329
+ fs.chmodSync(hookPath, 0o755);
330
+ console.log(`\n ✓ security pre-commit gate installed: ${path.relative(root, hookPath)}\n every commit now runs: atris security-review --staged\n`);
331
+ return 0;
332
+ } catch (e) { console.error(` ${e.message}`); return 2; }
333
+ }
334
+
335
+ function printHelp() {
336
+ console.log(`
337
+ atris security-review — deterministic secrets / PII / privacy scan (no LLM)
338
+
339
+ atris security-review scan git-tracked files (default)
340
+ atris security-review <path> scan a file or dir
341
+ atris security-review --staged scan staged changes (pre-commit gate)
342
+ atris security-review --strict also fail on MEDIUM (PII / personal paths)
343
+ atris security-review --json machine output / SOC 2 evidence artifact
344
+ atris security-review --md markdown evidence report
345
+ atris security-review --deep prompt a stronger model with framework + evidence
346
+ atris security-review --update-baseline
347
+ accept current findings in .security-review.baseline.json
348
+ atris security-review --no-baseline
349
+ ignore .security-review.baseline.json
350
+ atris security-review rules list the active detectors
351
+ atris security-review hook install a pre-commit gate
352
+
353
+ Scans for real-looking secrets, API keys, personal data, tracked sensitive
354
+ files, and code-exec review evidence. exit 0 = clean, 1 = found. Wire into
355
+ the autopilot/mission gate and CI.
356
+ `);
357
+ return 0;
358
+ }
359
+
360
+ module.exports = { securityReviewCommand };
package/commands/task.js CHANGED
@@ -616,7 +616,6 @@ function readGoalSources(root = process.cwd()) {
616
616
  const candidates = [
617
617
  path.join(root, 'atris', 'goals.md'),
618
618
  path.join(root, 'goals.md'),
619
- path.join(root, 'atris', 'wiki', 'concepts', 'atris-labs-goals.md'),
620
619
  ];
621
620
  for (const file of candidates) {
622
621
  if (!fs.existsSync(file)) continue;