@cccarv82/freya 1.0.21 → 1.0.23
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/cli/web-ui.js +62 -9
- package/cli/web.js +76 -10
- package/package.json +1 -1
package/cli/web-ui.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
(function () {
|
|
6
6
|
const $ = (id) => document.getElementById(id);
|
|
7
|
-
|
|
7
|
+
undefined
|
|
8
8
|
|
|
9
9
|
function applyTheme(theme) {
|
|
10
10
|
document.documentElement.setAttribute('data-theme', theme);
|
|
@@ -187,9 +187,23 @@
|
|
|
187
187
|
|
|
188
188
|
function saveLocal() {
|
|
189
189
|
localStorage.setItem('freya.dir', $('dir').value);
|
|
190
|
+
try { localStorage.setItem('freya.autoApply', state.autoApply ? '1' : '0'); } catch {}
|
|
191
|
+
try { localStorage.setItem('freya.autoRunReports', state.autoRunReports ? '1' : '0'); } catch {}
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
function loadLocal() {
|
|
195
|
+
try {
|
|
196
|
+
const v = localStorage.getItem('freya.autoApply');
|
|
197
|
+
if (v !== null) state.autoApply = v === '1';
|
|
198
|
+
const cb = $('autoApply');
|
|
199
|
+
if (cb) cb.checked = !!state.autoApply;
|
|
200
|
+
|
|
201
|
+
const v2 = localStorage.getItem('freya.autoRunReports');
|
|
202
|
+
if (v2 !== null) state.autoRunReports = v2 === '1';
|
|
203
|
+
const cb2 = $('autoRunReports');
|
|
204
|
+
if (cb2) cb2.checked = !!state.autoRunReports;
|
|
205
|
+
} catch {}
|
|
206
|
+
|
|
193
207
|
const def = (window.__FREYA_DEFAULT_DIR && window.__FREYA_DEFAULT_DIR !== '__FREYA_DEFAULT_DIR__')
|
|
194
208
|
? window.__FREYA_DEFAULT_DIR
|
|
195
209
|
: (localStorage.getItem('freya.dir') || './freya');
|
|
@@ -274,13 +288,22 @@
|
|
|
274
288
|
}
|
|
275
289
|
}
|
|
276
290
|
|
|
277
|
-
async function refreshReports() {
|
|
291
|
+
async function refreshReports(opts = {}) {
|
|
278
292
|
try {
|
|
279
293
|
const r = await api('/api/reports/list', { dir: dirOrDefault() });
|
|
280
294
|
state.reports = (r.reports || []).slice(0, 50);
|
|
281
295
|
renderReportsList();
|
|
282
|
-
|
|
283
|
-
|
|
296
|
+
|
|
297
|
+
const latest = state.reports && state.reports[0] ? state.reports[0] : null;
|
|
298
|
+
if (!latest) return;
|
|
299
|
+
|
|
300
|
+
if (opts.selectLatest) {
|
|
301
|
+
await selectReport(latest);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!state.selectedReport) {
|
|
306
|
+
await selectReport(latest);
|
|
284
307
|
}
|
|
285
308
|
} catch (e) {
|
|
286
309
|
// ignore
|
|
@@ -312,7 +335,7 @@
|
|
|
312
335
|
const r = await api('/api/init', { dir: dirOrDefault() });
|
|
313
336
|
setOut(r.output);
|
|
314
337
|
setLast(null);
|
|
315
|
-
await refreshReports();
|
|
338
|
+
await refreshReports({ selectLatest: true });
|
|
316
339
|
// Auto health after init
|
|
317
340
|
try { await doHealth(); } catch {}
|
|
318
341
|
setPill('ok', 'init ok');
|
|
@@ -331,7 +354,7 @@
|
|
|
331
354
|
const r = await api('/api/update', { dir: dirOrDefault() });
|
|
332
355
|
setOut(r.output);
|
|
333
356
|
setLast(null);
|
|
334
|
-
await refreshReports();
|
|
357
|
+
await refreshReports({ selectLatest: true });
|
|
335
358
|
// Auto health after update
|
|
336
359
|
try { await doHealth(); } catch {}
|
|
337
360
|
setPill('ok', 'update ok');
|
|
@@ -383,7 +406,7 @@
|
|
|
383
406
|
setOut(r.output);
|
|
384
407
|
setLast(r.reportPath || null);
|
|
385
408
|
if (r.reportText) state.lastText = r.reportText;
|
|
386
|
-
await refreshReports();
|
|
409
|
+
await refreshReports({ selectLatest: true });
|
|
387
410
|
setPill('ok', name + ' ok');
|
|
388
411
|
} catch (e) {
|
|
389
412
|
setPill('err', name + ' failed');
|
|
@@ -413,10 +436,26 @@
|
|
|
413
436
|
try {
|
|
414
437
|
saveLocal();
|
|
415
438
|
if (!state.lastText) throw new Error('Gere um relatório primeiro.');
|
|
439
|
+
|
|
440
|
+
// quick local warning (server also enforces)
|
|
441
|
+
const secretHints = [];
|
|
442
|
+
if (/ghp_[A-Za-z0-9]{20,}/.test(state.lastText)) secretHints.push('GitHub token');
|
|
443
|
+
if (/github_pat_[A-Za-z0-9_]{20,}/.test(state.lastText)) secretHints.push('GitHub fine-grained token');
|
|
444
|
+
if (/-----BEGIN [A-Z ]+PRIVATE KEY-----/.test(state.lastText)) secretHints.push('Private key');
|
|
445
|
+
if (/xox[baprs]-[A-Za-z0-9-]{10,}/.test(state.lastText)) secretHints.push('Slack token');
|
|
446
|
+
if (/AKIA[0-9A-Z]{16}/.test(state.lastText)) secretHints.push('AWS key');
|
|
447
|
+
|
|
448
|
+
const msg = secretHints.length
|
|
449
|
+
? 'ATENÇÃO: possível segredo detectado (' + secretHints.join(', ') + ').\n\nPublicar mesmo assim? (o Freya vai mascarar automaticamente)'
|
|
450
|
+
: 'Publicar o relatório selecionado?';
|
|
451
|
+
|
|
452
|
+
const ok = confirm(msg);
|
|
453
|
+
if (!ok) return;
|
|
454
|
+
|
|
416
455
|
const webhookUrl = target === 'discord' ? $('discord').value.trim() : $('teams').value.trim();
|
|
417
456
|
if (!webhookUrl) throw new Error('Configure o webhook antes.');
|
|
418
457
|
setPill('run', 'publish…');
|
|
419
|
-
await api('/api/publish', { webhookUrl, text: state.lastText, mode: 'chunks' });
|
|
458
|
+
await api('/api/publish', { webhookUrl, text: state.lastText, mode: 'chunks', allowSecrets: true });
|
|
420
459
|
setPill('ok', 'published');
|
|
421
460
|
} catch (e) {
|
|
422
461
|
setPill('err', 'publish failed');
|
|
@@ -443,6 +482,18 @@
|
|
|
443
482
|
}
|
|
444
483
|
}
|
|
445
484
|
|
|
485
|
+
function toggleAutoRunReports() {
|
|
486
|
+
const cb = $('autoRunReports');
|
|
487
|
+
state.autoRunReports = cb ? !!cb.checked : false;
|
|
488
|
+
try { localStorage.setItem('freya.autoRunReports', state.autoRunReports ? '1' : '0'); } catch {}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function toggleAutoApply() {
|
|
492
|
+
const cb = $('autoApply');
|
|
493
|
+
state.autoApply = cb ? !!cb.checked : true;
|
|
494
|
+
try { localStorage.setItem('freya.autoApply', state.autoApply ? '1' : '0'); } catch {}
|
|
495
|
+
}
|
|
496
|
+
|
|
446
497
|
async function saveAndPlan() {
|
|
447
498
|
try {
|
|
448
499
|
const ta = $('inboxText');
|
|
@@ -500,7 +551,7 @@
|
|
|
500
551
|
}
|
|
501
552
|
|
|
502
553
|
setOut(out);
|
|
503
|
-
await refreshReports();
|
|
554
|
+
await refreshReports({ selectLatest: true });
|
|
504
555
|
setPill('ok', 'done');
|
|
505
556
|
setTimeout(() => setPill('ok', 'idle'), 800);
|
|
506
557
|
} catch (e) {
|
|
@@ -594,6 +645,8 @@
|
|
|
594
645
|
window.toggleTheme = toggleTheme;
|
|
595
646
|
window.saveInbox = saveInbox;
|
|
596
647
|
window.saveAndPlan = saveAndPlan;
|
|
648
|
+
window.toggleAutoApply = toggleAutoApply;
|
|
649
|
+
window.toggleAutoRunReports = toggleAutoRunReports;
|
|
597
650
|
window.applyPlan = applyPlan;
|
|
598
651
|
window.runSuggestedReports = runSuggestedReports;
|
|
599
652
|
})();
|
package/cli/web.js
CHANGED
|
@@ -228,6 +228,44 @@ function escapeJsonControlChars(jsonText) {
|
|
|
228
228
|
return out.join('');
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
function scanSecrets(text) {
|
|
232
|
+
const t = String(text || '');
|
|
233
|
+
const findings = [];
|
|
234
|
+
|
|
235
|
+
const rules = [
|
|
236
|
+
{ name: 'GitHub token (ghp_)', re: /ghp_[A-Za-z0-9]{20,}/g },
|
|
237
|
+
{ name: 'GitHub fine-grained token (github_pat_)', re: /github_pat_[A-Za-z0-9_]{20,}/g },
|
|
238
|
+
{ name: 'Slack token (xox*)', re: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
|
|
239
|
+
{ name: 'AWS Access Key (AKIA)', re: /AKIA[0-9A-Z]{16}/g },
|
|
240
|
+
{ name: 'Private key block', re: /-----BEGIN [A-Z ]+PRIVATE KEY-----/g },
|
|
241
|
+
{ name: 'Discord webhook URL', re: /https?:\/\/(?:canary\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[^\s]+/g },
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
for (const r of rules) {
|
|
245
|
+
const m = t.match(r.re);
|
|
246
|
+
if (m && m.length) findings.push({ rule: r.name, count: m.length });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Basic heuristic: long high-entropy strings
|
|
250
|
+
const long = t.match(/[A-Za-z0-9+\/=]{60,}/g);
|
|
251
|
+
if (long && long.length) findings.push({ rule: 'High-entropy blob (heuristic)', count: long.length });
|
|
252
|
+
|
|
253
|
+
return findings;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function redactSecrets(text) {
|
|
257
|
+
let out = String(text || '');
|
|
258
|
+
const replacements = [
|
|
259
|
+
/ghp_[A-Za-z0-9]{20,}/g,
|
|
260
|
+
/github_pat_[A-Za-z0-9_]{20,}/g,
|
|
261
|
+
/xox[baprs]-[A-Za-z0-9-]{10,}/g,
|
|
262
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
263
|
+
/-----BEGIN [A-Z ]+PRIVATE KEY-----[sS]*?-----END [A-Z ]+PRIVATE KEY-----/g,
|
|
264
|
+
];
|
|
265
|
+
for (const re of replacements) out = out.replace(re, '[REDACTED]');
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
|
|
231
269
|
async function publishRobust(webhookUrl, text, opts = {}) {
|
|
232
270
|
const u = new URL(webhookUrl);
|
|
233
271
|
const isDiscord = u.hostname.includes('discord.com') || u.hostname.includes('discordapp.com');
|
|
@@ -422,7 +460,19 @@ function buildHtml(safeDefault) {
|
|
|
422
460
|
<button class="btn primary sideBtn" onclick="saveAndPlan()">Save + Process (Agents)</button>
|
|
423
461
|
<button class="btn sideBtn" onclick="runSuggestedReports()">Run suggested reports</button>
|
|
424
462
|
</div>
|
|
425
|
-
|
|
463
|
+
|
|
464
|
+
<div style="height:10px"></div>
|
|
465
|
+
<label style="display:flex; align-items:center; gap:10px; user-select:none">
|
|
466
|
+
<input id="autoApply" type="checkbox" checked style="width:auto" onchange="toggleAutoApply()" />
|
|
467
|
+
Auto-apply plan
|
|
468
|
+
</label>
|
|
469
|
+
<div class="help">Quando ligado, o Save+Process já aplica tasks/blockers automaticamente.</div>
|
|
470
|
+
|
|
471
|
+
<label style="display:flex; align-items:center; gap:10px; user-select:none; margin-top:10px">
|
|
472
|
+
<input id="autoRunReports" type="checkbox" style="width:auto" onchange="toggleAutoRunReports()" />
|
|
473
|
+
Auto-run suggested reports
|
|
474
|
+
</label>
|
|
475
|
+
<div class="help">Quando ligado, após aplicar o plano, ele também roda os reports sugeridos automaticamente.</div>
|
|
426
476
|
</div>
|
|
427
477
|
</aside>
|
|
428
478
|
|
|
@@ -908,7 +958,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
908
958
|
const type = String(a.type || '').trim();
|
|
909
959
|
|
|
910
960
|
if (type === 'create_task') {
|
|
911
|
-
const description =
|
|
961
|
+
const description = normalizeWhitespace(a.description);
|
|
912
962
|
const category = String(a.category || '').trim();
|
|
913
963
|
const priorityRaw = String(a.priority || '').trim().toLowerCase();
|
|
914
964
|
const priority = (priorityRaw === 'high' || priorityRaw === 'medium' || priorityRaw === 'low') ? priorityRaw : undefined;
|
|
@@ -918,8 +968,8 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
918
968
|
}
|
|
919
969
|
|
|
920
970
|
if (type === 'create_blocker') {
|
|
921
|
-
const title =
|
|
922
|
-
const notes =
|
|
971
|
+
const title = normalizeWhitespace(a.title);
|
|
972
|
+
const notes = normalizeWhitespace(a.notes);
|
|
923
973
|
let severity = String(a.severity || '').trim().toUpperCase();
|
|
924
974
|
if (!validSev.has(severity)) {
|
|
925
975
|
if (severity.includes('CRIT')) severity = 'CRITICAL';
|
|
@@ -1014,8 +1064,12 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1014
1064
|
if (!Array.isArray(blockerLog.blockers)) blockerLog.blockers = [];
|
|
1015
1065
|
if (typeof blockerLog.schemaVersion !== 'number') blockerLog.schemaVersion = 1;
|
|
1016
1066
|
|
|
1067
|
+
function normalizeWhitespace(t) {
|
|
1068
|
+
return String(t || '').replace(/\s+/g, ' ').trim();
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1017
1071
|
function normalizeTextForKey(t) {
|
|
1018
|
-
return
|
|
1072
|
+
return normalizeWhitespace(t).toLowerCase();
|
|
1019
1073
|
}
|
|
1020
1074
|
|
|
1021
1075
|
function sha1(text) {
|
|
@@ -1079,7 +1133,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1079
1133
|
|
|
1080
1134
|
if (type === 'create_task') {
|
|
1081
1135
|
if (applyMode !== 'all' && applyMode !== 'tasks') continue;
|
|
1082
|
-
const description =
|
|
1136
|
+
const description = normalizeWhitespace(a.description);
|
|
1083
1137
|
if (!description) continue;
|
|
1084
1138
|
const key = sha1(normalizeTextForKey(description));
|
|
1085
1139
|
if (existingTaskKeys24h.has(key)) { applied.tasksSkipped++; continue; }
|
|
@@ -1100,10 +1154,10 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1100
1154
|
|
|
1101
1155
|
if (type === 'create_blocker') {
|
|
1102
1156
|
if (applyMode !== 'all' && applyMode !== 'blockers') continue;
|
|
1103
|
-
const title =
|
|
1157
|
+
const title = normalizeWhitespace(a.title);
|
|
1104
1158
|
const key = sha1(normalizeTextForKey(title));
|
|
1105
1159
|
if (existingBlockerKeys24h.has(key)) { applied.blockersSkipped++; continue; }
|
|
1106
|
-
const notes =
|
|
1160
|
+
const notes = normalizeWhitespace(a.notes);
|
|
1107
1161
|
if (!title) continue;
|
|
1108
1162
|
const severity = normSeverity(a.severity);
|
|
1109
1163
|
const blocker = {
|
|
@@ -1219,12 +1273,24 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1219
1273
|
const webhookUrl = payload.webhookUrl;
|
|
1220
1274
|
const text = payload.text;
|
|
1221
1275
|
const mode = payload.mode || 'chunks';
|
|
1276
|
+
const allowSecrets = !!payload.allowSecrets;
|
|
1222
1277
|
if (!webhookUrl) return safeJson(res, 400, { error: 'Missing webhookUrl' });
|
|
1223
1278
|
if (!text) return safeJson(res, 400, { error: 'Missing text' });
|
|
1224
1279
|
|
|
1280
|
+
const findings = scanSecrets(text);
|
|
1281
|
+
if (findings.length && !allowSecrets) {
|
|
1282
|
+
return safeJson(res, 400, {
|
|
1283
|
+
error: 'Potential secrets detected. Refusing to publish.',
|
|
1284
|
+
details: JSON.stringify(findings, null, 2),
|
|
1285
|
+
hint: 'Remova/mascare tokens ou confirme a publicação mesmo assim.'
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const safeText = findings.length ? redactSecrets(text) : text;
|
|
1290
|
+
|
|
1225
1291
|
try {
|
|
1226
|
-
const result = await publishRobust(webhookUrl,
|
|
1227
|
-
return safeJson(res, 200, result);
|
|
1292
|
+
const result = await publishRobust(webhookUrl, safeText, { mode });
|
|
1293
|
+
return safeJson(res, 200, { ...result, redacted: findings.length > 0, findings });
|
|
1228
1294
|
} catch (e) {
|
|
1229
1295
|
return safeJson(res, 400, { error: e.message || String(e) });
|
|
1230
1296
|
}
|