@node9/proxy 1.11.12 → 1.11.13
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/README.md +142 -244
- package/dist/cli.js +488 -391
- package/dist/cli.mjs +488 -391
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5735,6 +5735,12 @@ var init_ui = __esm({
|
|
|
5735
5735
|
text-align: center;
|
|
5736
5736
|
}
|
|
5737
5737
|
/* results */
|
|
5738
|
+
.scan-date-range {
|
|
5739
|
+
text-align: center;
|
|
5740
|
+
font-size: 11px;
|
|
5741
|
+
color: var(--muted);
|
|
5742
|
+
padding: 6px 0 10px;
|
|
5743
|
+
}
|
|
5738
5744
|
.scan-summary-row {
|
|
5739
5745
|
display: grid;
|
|
5740
5746
|
grid-template-columns: repeat(4, 1fr);
|
|
@@ -7627,13 +7633,10 @@ var init_ui = __esm({
|
|
|
7627
7633
|
const res = await fetch('/scan', { headers: { 'X-Node9-Token': CSRF_TOKEN } });
|
|
7628
7634
|
if (!res.ok) return;
|
|
7629
7635
|
const data = await res.json();
|
|
7630
|
-
if (data.status !== 'complete') return;
|
|
7636
|
+
if (data.status !== 'complete' || !data.summary) return;
|
|
7631
7637
|
_scanData = data;
|
|
7632
|
-
const
|
|
7633
|
-
const total =
|
|
7634
|
-
sources.reduce((n, s) => n + (s.findings || []).length, 0) +
|
|
7635
|
-
sources.reduce((n, s) => n + (s.leaks || []).length, 0) +
|
|
7636
|
-
sources.reduce((n, s) => n + (s.loops || []).length, 0);
|
|
7638
|
+
const v = data.summary.byVerdict || {};
|
|
7639
|
+
const total = (v.blocked || 0) + (v.supervised || 0) + (v.leaks || 0) + (v.loops || 0);
|
|
7637
7640
|
if (total > 0) {
|
|
7638
7641
|
const btn = document.getElementById('btn-scan-history');
|
|
7639
7642
|
if (btn && !btn.querySelector('.scan-badge')) {
|
|
@@ -7738,42 +7741,26 @@ var init_ui = __esm({
|
|
|
7738
7741
|
}
|
|
7739
7742
|
|
|
7740
7743
|
function renderScanResults(data) {
|
|
7741
|
-
if (!data || data.status !== 'complete') {
|
|
7744
|
+
if (!data || data.status !== 'complete' || !data.summary) {
|
|
7742
7745
|
return '<div style="text-align:center;color:var(--muted);padding:32px">No data returned.</div>';
|
|
7743
7746
|
}
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
const
|
|
7749
|
-
const
|
|
7750
|
-
const
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
const
|
|
7754
|
-
const
|
|
7755
|
-
(f) => f.verdict !== 'block' && f.ruleSource !== 'user'
|
|
7756
|
-
);
|
|
7757
|
-
const yourRules = allFindings.filter((f) => f.ruleSource === 'user');
|
|
7758
|
-
|
|
7759
|
-
// Loop cost estimate: each wasted iteration ~2K tokens at Sonnet pricing
|
|
7760
|
-
const LOOP_THRESHOLD = 3;
|
|
7761
|
-
const COST_PER_ITER = 0.006;
|
|
7762
|
-
const wastedIters = allLoops.reduce(
|
|
7763
|
-
(sum, l) => sum + Math.max(0, (l.count || 0) - LOOP_THRESHOLD),
|
|
7764
|
-
0
|
|
7765
|
-
);
|
|
7766
|
-
const loopWastedUSD = wastedIters * COST_PER_ITER;
|
|
7767
|
-
|
|
7768
|
-
const totalCost = s.totalCostUSD || 0;
|
|
7747
|
+
// Server-computed ScanSummary (see src/scan-summary.ts).
|
|
7748
|
+
// This renderer is a pure presentation layer \u2014 no categorization here.
|
|
7749
|
+
const summary = data.summary;
|
|
7750
|
+
const stats = summary.stats || {};
|
|
7751
|
+
const byVerdict = summary.byVerdict || { blocked: 0, supervised: 0, leaks: 0, loops: 0 };
|
|
7752
|
+
const byAgent = summary.byAgent || [];
|
|
7753
|
+
const sections = summary.sections || [];
|
|
7754
|
+
const leaks = summary.leaks || [];
|
|
7755
|
+
const loops = summary.loops || [];
|
|
7756
|
+
const loopWastedUSD = summary.loopWastedUSD || 0;
|
|
7757
|
+
const totalCost = stats.totalCostUSD || 0;
|
|
7769
7758
|
|
|
7770
7759
|
function fmtUSD(n) {
|
|
7771
7760
|
if (!n || n < 0.001) return null;
|
|
7772
7761
|
if (n < 1) return '$' + n.toFixed(3);
|
|
7773
7762
|
return '$' + n.toFixed(2);
|
|
7774
7763
|
}
|
|
7775
|
-
|
|
7776
|
-
// \u2500\u2500 Hero stats row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7777
7764
|
const costStr = fmtUSD(totalCost);
|
|
7778
7765
|
const savingsStr = fmtUSD(loopWastedUSD);
|
|
7779
7766
|
|
|
@@ -7795,10 +7782,46 @@ var init_ui = __esm({
|
|
|
7795
7782
|
);
|
|
7796
7783
|
}
|
|
7797
7784
|
|
|
7798
|
-
|
|
7785
|
+
// \u2500\u2500 Date range header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7786
|
+
function fmtShort(iso) {
|
|
7787
|
+
if (!iso) return null;
|
|
7788
|
+
try {
|
|
7789
|
+
return new Date(iso).toLocaleDateString([], {
|
|
7790
|
+
month: 'short',
|
|
7791
|
+
day: 'numeric',
|
|
7792
|
+
year: 'numeric',
|
|
7793
|
+
});
|
|
7794
|
+
} catch {
|
|
7795
|
+
return null;
|
|
7796
|
+
}
|
|
7797
|
+
}
|
|
7798
|
+
const firstLabel = fmtShort(stats.firstDate);
|
|
7799
|
+
const lastLabel = fmtShort(stats.lastDate);
|
|
7800
|
+
let daySpan = '';
|
|
7801
|
+
if (stats.firstDate && stats.lastDate) {
|
|
7802
|
+
const days =
|
|
7803
|
+
Math.round(
|
|
7804
|
+
(new Date(stats.lastDate).getTime() - new Date(stats.firstDate).getTime()) /
|
|
7805
|
+
(1000 * 60 * 60 * 24)
|
|
7806
|
+
) + 1;
|
|
7807
|
+
if (days > 1) daySpan = ' \xB7 ' + days + ' day' + (days !== 1 ? 's' : '');
|
|
7808
|
+
}
|
|
7809
|
+
const dateRangeHtml =
|
|
7810
|
+
firstLabel && lastLabel
|
|
7811
|
+
? '<div class="scan-date-range">\u{1F4C5} ' +
|
|
7812
|
+
esc(firstLabel) +
|
|
7813
|
+
' \u2013 ' +
|
|
7814
|
+
esc(lastLabel) +
|
|
7815
|
+
esc(daySpan) +
|
|
7816
|
+
'</div>'
|
|
7817
|
+
: '';
|
|
7818
|
+
|
|
7819
|
+
// \u2500\u2500 Hero stats row (by VERDICT \u2014 matches terminal) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7820
|
+
const statsHtml =
|
|
7821
|
+
dateRangeHtml +
|
|
7799
7822
|
'<div class="scan-summary-row">' +
|
|
7800
7823
|
'<div class="scan-stat">' +
|
|
7801
|
-
statVal(
|
|
7824
|
+
statVal(stats.sessions || 0, '') +
|
|
7802
7825
|
'<div class="scan-stat-label">Sessions</div></div>' +
|
|
7803
7826
|
(totalCost > 0
|
|
7804
7827
|
? '<div class="scan-stat">' +
|
|
@@ -7806,19 +7829,16 @@ var init_ui = __esm({
|
|
|
7806
7829
|
'<div class="scan-stat-label">AI Spend</div></div>'
|
|
7807
7830
|
: '') +
|
|
7808
7831
|
'<div class="scan-stat">' +
|
|
7809
|
-
statVal(blocked
|
|
7832
|
+
statVal(byVerdict.blocked || 0, byVerdict.blocked ? 'danger' : 'ok') +
|
|
7810
7833
|
'<div class="scan-stat-label">Blocked</div></div>' +
|
|
7811
7834
|
'<div class="scan-stat">' +
|
|
7812
|
-
statVal(supervised
|
|
7835
|
+
statVal(byVerdict.supervised || 0, byVerdict.supervised ? 'warning' : 'ok') +
|
|
7813
7836
|
'<div class="scan-stat-label">Supervised</div></div>' +
|
|
7814
7837
|
'<div class="scan-stat">' +
|
|
7815
|
-
statVal(
|
|
7816
|
-
'<div class="scan-stat-label">Your Rules</div></div>' +
|
|
7817
|
-
'<div class="scan-stat">' +
|
|
7818
|
-
statVal(allLeaks.length, allLeaks.length ? 'danger' : 'ok') +
|
|
7838
|
+
statVal(byVerdict.leaks || 0, byVerdict.leaks ? 'danger' : 'ok') +
|
|
7819
7839
|
'<div class="scan-stat-label">Leaks</div></div>' +
|
|
7820
7840
|
'<div class="scan-stat">' +
|
|
7821
|
-
statVal(
|
|
7841
|
+
statVal(byVerdict.loops || 0, byVerdict.loops ? 'warning' : 'ok') +
|
|
7822
7842
|
'<div class="scan-stat-label">Loops</div></div>' +
|
|
7823
7843
|
'</div>' +
|
|
7824
7844
|
(savingsStr
|
|
@@ -7827,56 +7847,103 @@ var init_ui = __esm({
|
|
|
7827
7847
|
'</span> in unnecessary LLM turns</div>'
|
|
7828
7848
|
: '');
|
|
7829
7849
|
|
|
7830
|
-
|
|
7850
|
+
const hasNothing = !sections.length && !leaks.length && !loops.length;
|
|
7851
|
+
if (hasNothing) {
|
|
7831
7852
|
return (
|
|
7832
|
-
|
|
7853
|
+
statsHtml +
|
|
7833
7854
|
'<div style="text-align:center;color:#57ab5a;padding:28px 0;font-size:13px;line-height:1.8">\u2705 <strong>No issues found</strong><br><span style="font-size:11px;color:var(--muted)">Scanned ' +
|
|
7834
|
-
(
|
|
7835
|
-
' sessions
|
|
7836
|
-
sources.length +
|
|
7837
|
-
' AI source(s)</span></div>'
|
|
7855
|
+
(stats.sessions || 0) +
|
|
7856
|
+
' sessions</span></div>'
|
|
7838
7857
|
);
|
|
7839
7858
|
}
|
|
7840
7859
|
|
|
7841
|
-
// \u2500\u2500
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7860
|
+
// \u2500\u2500 Per-agent breakdown (server-computed byAgent) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7861
|
+
const agentBreakdown = byAgent.length
|
|
7862
|
+
? '<div class="scan-agent-group">' +
|
|
7863
|
+
byAgent
|
|
7864
|
+
.map(
|
|
7865
|
+
(a) =>
|
|
7866
|
+
'<div class="scan-agent-row">' +
|
|
7867
|
+
'<span class="scan-agent-name">' +
|
|
7868
|
+
esc(a.icon || '') +
|
|
7869
|
+
' ' +
|
|
7870
|
+
esc(a.label) +
|
|
7871
|
+
'</span>' +
|
|
7872
|
+
'<span class="scan-agent-stat">' +
|
|
7873
|
+
a.sessions +
|
|
7874
|
+
' session' +
|
|
7875
|
+
(a.sessions !== 1 ? 's' : '') +
|
|
7876
|
+
'</span>' +
|
|
7877
|
+
'<span class="scan-agent-sep">\xB7</span>' +
|
|
7878
|
+
'<span class="scan-agent-stat">' +
|
|
7879
|
+
a.findings +
|
|
7880
|
+
' finding' +
|
|
7881
|
+
(a.findings !== 1 ? 's' : '') +
|
|
7882
|
+
'</span>' +
|
|
7883
|
+
'</div>'
|
|
7884
|
+
)
|
|
7885
|
+
.join('') +
|
|
7886
|
+
'</div>'
|
|
7887
|
+
: '';
|
|
7888
|
+
|
|
7889
|
+
// \u2500\u2500 Rule row renderer \u2014 shared between rule sections + leaks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7890
|
+
// Determines bar-max across all sections so relative counts stay honest.
|
|
7891
|
+
let maxBar = 1;
|
|
7892
|
+
for (const section of sections) {
|
|
7893
|
+
for (const rule of section.rules || []) {
|
|
7894
|
+
if (rule.findings.length > maxBar) maxBar = rule.findings.length;
|
|
7848
7895
|
}
|
|
7849
|
-
return [...map.entries()].sort((a, b) => b[1].length - a[1].length);
|
|
7850
7896
|
}
|
|
7897
|
+
// Group leaks by patternName for display
|
|
7898
|
+
const leaksByPattern = (function () {
|
|
7899
|
+
const m = new Map();
|
|
7900
|
+
for (const l of leaks) {
|
|
7901
|
+
const k = l.patternName || 'DLP';
|
|
7902
|
+
if (!m.has(k)) m.set(k, []);
|
|
7903
|
+
m.get(k).push(l);
|
|
7904
|
+
}
|
|
7905
|
+
for (const [, group] of m) if (group.length > maxBar) maxBar = group.length;
|
|
7906
|
+
return [...m.entries()].sort((a, b) => b[1].length - a[1].length);
|
|
7907
|
+
})();
|
|
7851
7908
|
|
|
7852
|
-
function findingRow(
|
|
7853
|
-
const date = new Date(
|
|
7909
|
+
function findingRow(timestamp, text) {
|
|
7910
|
+
const date = new Date(timestamp).toLocaleDateString([], {
|
|
7854
7911
|
month: 'short',
|
|
7855
7912
|
day: 'numeric',
|
|
7856
7913
|
});
|
|
7857
|
-
const cmdText =
|
|
7858
|
-
type === 'leak' ? esc(item.sample || '') : esc(truncate(item.command || '', 120));
|
|
7859
7914
|
return (
|
|
7860
7915
|
'<div class="scan-finding-row"><span class="scan-finding-ts">' +
|
|
7861
7916
|
date +
|
|
7862
7917
|
'</span><span class="scan-finding-cmd">' +
|
|
7863
|
-
|
|
7918
|
+
text +
|
|
7864
7919
|
'</span></div>'
|
|
7865
7920
|
);
|
|
7866
7921
|
}
|
|
7867
7922
|
|
|
7868
|
-
function
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
: (i) => (i.rule || 'unknown').replace(/^shield:[^:]+:/, '');
|
|
7873
|
-
const grouped = groupByKey(items, keyFn);
|
|
7874
|
-
return grouped
|
|
7875
|
-
.map(([rule, group]) => {
|
|
7876
|
-
const count = group.length;
|
|
7923
|
+
function ruleCardsForSection(section) {
|
|
7924
|
+
return (section.rules || [])
|
|
7925
|
+
.map((rule) => {
|
|
7926
|
+
const count = rule.findings.length;
|
|
7877
7927
|
const barPct = Math.round((count / maxBar) * 100);
|
|
7878
7928
|
const detailId = 'detail-' + Math.random().toString(36).slice(2);
|
|
7879
|
-
const
|
|
7929
|
+
const badgeClass =
|
|
7930
|
+
section.sourceType === 'user'
|
|
7931
|
+
? 'badge-user'
|
|
7932
|
+
: rule.verdict === 'block'
|
|
7933
|
+
? 'badge-block'
|
|
7934
|
+
: 'badge-review';
|
|
7935
|
+
const badgeLabel =
|
|
7936
|
+
section.sourceType === 'user'
|
|
7937
|
+
? rule.verdict === 'block'
|
|
7938
|
+
? 'YOUR BLOCK'
|
|
7939
|
+
: 'YOUR RULE'
|
|
7940
|
+
: rule.verdict === 'block'
|
|
7941
|
+
? 'BLOCK'
|
|
7942
|
+
: 'REVIEW';
|
|
7943
|
+
const barColor = section.sourceType === 'user' ? ';background:#388bfd33' : '';
|
|
7944
|
+
const rows = rule.findings
|
|
7945
|
+
.map((f) => findingRow(f.timestamp, esc(truncate(f.command || '', 120))))
|
|
7946
|
+
.join('');
|
|
7880
7947
|
return (
|
|
7881
7948
|
'<div class="scan-rule-row" onclick="var d=document.getElementById(\\'' +
|
|
7882
7949
|
detailId +
|
|
@@ -7887,12 +7954,12 @@ var init_ui = __esm({
|
|
|
7887
7954
|
badgeLabel +
|
|
7888
7955
|
'</span>' +
|
|
7889
7956
|
'<span class="scan-rule-name">' +
|
|
7890
|
-
esc(rule) +
|
|
7957
|
+
esc(rule.name) +
|
|
7891
7958
|
'</span>' +
|
|
7892
7959
|
'<span class="scan-rule-bar-wrap"><span class="scan-rule-bar" style="width:' +
|
|
7893
7960
|
barPct +
|
|
7894
7961
|
'%' +
|
|
7895
|
-
|
|
7962
|
+
barColor +
|
|
7896
7963
|
'"></span></span>' +
|
|
7897
7964
|
'<span class="scan-rule-count">' +
|
|
7898
7965
|
count +
|
|
@@ -7908,98 +7975,95 @@ var init_ui = __esm({
|
|
|
7908
7975
|
.join('');
|
|
7909
7976
|
}
|
|
7910
7977
|
|
|
7911
|
-
// \u2500\u2500
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
)
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7978
|
+
// \u2500\u2500 Section headings by sourceType / id \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7979
|
+
function sectionHeading(section) {
|
|
7980
|
+
if (section.id === 'default') {
|
|
7981
|
+
return '\u{1F441} ' + esc(section.label) + ' \u2014 built-in, always on';
|
|
7982
|
+
}
|
|
7983
|
+
if (section.sourceType === 'shield') {
|
|
7984
|
+
return (
|
|
7985
|
+
'\u{1F6E1} ' +
|
|
7986
|
+
esc(section.label) +
|
|
7987
|
+
' shield' +
|
|
7988
|
+
(section.subtitle ? ' \u2014 ' + esc(section.subtitle) : '')
|
|
7989
|
+
);
|
|
7990
|
+
}
|
|
7991
|
+
if (section.id === 'cloud') {
|
|
7992
|
+
return '\u2601 Cloud Policy \u2014 synced from node9';
|
|
7993
|
+
}
|
|
7994
|
+
return '\u2705 Your Rules \u2014 working as configured';
|
|
7995
|
+
}
|
|
7996
|
+
function sectionColor(section) {
|
|
7997
|
+
if (section.blockedCount > 0) return '#e5534b';
|
|
7998
|
+
if (section.sourceType === 'user') return '#58a6ff';
|
|
7999
|
+
return 'var(--muted)';
|
|
8000
|
+
}
|
|
7920
8001
|
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
const
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
const findingsTotal = f + l + lp;
|
|
8002
|
+
let html = statsHtml + agentBreakdown;
|
|
8003
|
+
|
|
8004
|
+
// \u2500\u2500 Leaks (shown first: credential exposure is highest-severity) \u2500\u2500\u2500\u2500\u2500
|
|
8005
|
+
if (leaksByPattern.length) {
|
|
8006
|
+
html +=
|
|
8007
|
+
'<div class="scan-rule-section-label" style="color:#e5534b">\u{1F511} Credential Leaks \u2014 secrets found in tool calls</div>';
|
|
8008
|
+
html += leaksByPattern
|
|
8009
|
+
.map(([pattern, group]) => {
|
|
8010
|
+
const count = group.length;
|
|
8011
|
+
const barPct = Math.round((count / maxBar) * 100);
|
|
8012
|
+
const detailId = 'detail-' + Math.random().toString(36).slice(2);
|
|
8013
|
+
const rows = group
|
|
8014
|
+
.map((l) => findingRow(l.timestamp, esc(l.redactedSample || '')))
|
|
8015
|
+
.join('');
|
|
7936
8016
|
return (
|
|
7937
|
-
'<div class="scan-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
' ' +
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
'<span class="scan-agent-stat">' +
|
|
7944
|
-
(src.sessions || 0) +
|
|
7945
|
-
' session' +
|
|
7946
|
-
((src.sessions || 0) !== 1 ? 's' : '') +
|
|
8017
|
+
'<div class="scan-rule-row" onclick="var d=document.getElementById(\\'' +
|
|
8018
|
+
detailId +
|
|
8019
|
+
"');var a=this.querySelector('.scan-rule-arrow');d.style.display=d.style.display===''?'none':'';a.textContent=d.style.display===''?'\u25B2':'\u25BC';\\">" +
|
|
8020
|
+
'<span class="scan-verdict-badge badge-dlp">DLP</span>' +
|
|
8021
|
+
'<span class="scan-rule-name">' +
|
|
8022
|
+
esc(pattern) +
|
|
7947
8023
|
'</span>' +
|
|
7948
|
-
'<span class="scan-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
'
|
|
7952
|
-
|
|
8024
|
+
'<span class="scan-rule-bar-wrap"><span class="scan-rule-bar" style="width:' +
|
|
8025
|
+
barPct +
|
|
8026
|
+
'%"></span></span>' +
|
|
8027
|
+
'<span class="scan-rule-count">' +
|
|
8028
|
+
count +
|
|
7953
8029
|
'</span>' +
|
|
8030
|
+
'<span class="scan-rule-arrow">\u25BC</span>' +
|
|
8031
|
+
'</div><div id="' +
|
|
8032
|
+
detailId +
|
|
8033
|
+
'" style="display:none" class="scan-rule-detail">' +
|
|
8034
|
+
rows +
|
|
7954
8035
|
'</div>'
|
|
7955
8036
|
);
|
|
7956
|
-
})
|
|
7957
|
-
|
|
7958
|
-
return '<div class="scan-agent-group">' + rows.join('') + '</div>';
|
|
7959
|
-
})();
|
|
7960
|
-
|
|
7961
|
-
let html = stats + agentBreakdown;
|
|
7962
|
-
|
|
7963
|
-
if (allLeaks.length) {
|
|
7964
|
-
html +=
|
|
7965
|
-
'<div class="scan-rule-section-label" style="color:#e5534b">\u{1F511} Credential Leaks \u2014 secrets found in tool calls</div>';
|
|
7966
|
-
html += ruleSection(allLeaks, 'leak', 'badge-dlp', 'DLP', maxBar);
|
|
7967
|
-
}
|
|
7968
|
-
|
|
7969
|
-
if (blocked.length) {
|
|
7970
|
-
html +=
|
|
7971
|
-
'<div class="scan-rule-section-label" style="color:#e5534b">\u{1F6D1} Blocked \u2014 Node9 would have stopped these</div>';
|
|
7972
|
-
html += ruleSection(blocked, 'risk', 'badge-block', 'BLOCK', maxBar);
|
|
7973
|
-
}
|
|
7974
|
-
|
|
7975
|
-
if (supervised.length) {
|
|
7976
|
-
html +=
|
|
7977
|
-
'<div class="scan-rule-section-label">\u{1F441} Supervised \u2014 Node9 would have asked you first</div>';
|
|
7978
|
-
html += ruleSection(supervised, 'risk', 'badge-review', 'REVIEW', maxBar);
|
|
8037
|
+
})
|
|
8038
|
+
.join('');
|
|
7979
8039
|
}
|
|
7980
8040
|
|
|
7981
|
-
|
|
8041
|
+
// \u2500\u2500 Sections (pre-grouped by source on the server) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
8042
|
+
for (const section of sections) {
|
|
7982
8043
|
html +=
|
|
7983
|
-
'<div class="scan-rule-section-label" style="color
|
|
7984
|
-
|
|
8044
|
+
'<div class="scan-rule-section-label" style="color:' +
|
|
8045
|
+
sectionColor(section) +
|
|
8046
|
+
'">' +
|
|
8047
|
+
sectionHeading(section) +
|
|
8048
|
+
'</div>';
|
|
8049
|
+
html += ruleCardsForSection(section);
|
|
7985
8050
|
}
|
|
7986
8051
|
|
|
7987
|
-
// Loops: single collapsed summary card, expand to see all groups
|
|
7988
|
-
if (
|
|
7989
|
-
const totalReps =
|
|
8052
|
+
// \u2500\u2500 Loops: single collapsed summary card, expand to see all groups \u2500\u2500\u2500
|
|
8053
|
+
if (loops.length) {
|
|
8054
|
+
const totalReps = loops.reduce((sum, l) => sum + (l.count || 0), 0);
|
|
7990
8055
|
const loopsDetailId = 'loops-detail-' + Math.random().toString(36).slice(2);
|
|
7991
8056
|
const savingNote = savingsStr
|
|
7992
8057
|
? ' \xB7 <span style="color:var(--warning)">~' + savingsStr + ' wasted</span>'
|
|
7993
8058
|
: '';
|
|
7994
|
-
const
|
|
8059
|
+
const maxLoopCount = Math.max(...loops.map((l) => l.count || 0), 1);
|
|
8060
|
+
const loopGroupRows = loops
|
|
7995
8061
|
.map((item) => {
|
|
7996
8062
|
const date = new Date(item.timestamp).toLocaleDateString([], {
|
|
7997
8063
|
month: 'short',
|
|
7998
8064
|
day: 'numeric',
|
|
7999
8065
|
});
|
|
8000
|
-
const barPct = Math.round(
|
|
8001
|
-
(item.count / Math.max(...allLoops.map((l) => l.count), 1)) * 100
|
|
8002
|
-
);
|
|
8066
|
+
const barPct = Math.round((item.count / maxLoopCount) * 100);
|
|
8003
8067
|
const detailId = 'detail-' + Math.random().toString(36).slice(2);
|
|
8004
8068
|
return (
|
|
8005
8069
|
'<div class="scan-rule-row" onclick="event.stopPropagation();var d=document.getElementById(\\'' +
|
|
@@ -8037,9 +8101,9 @@ var init_ui = __esm({
|
|
|
8037
8101
|
"');var a=this.querySelector('.scan-rule-arrow');d.style.display=d.style.display===''?'none':'';a.textContent=d.style.display===''?'\u25B2':'\u25BC';\\">" +
|
|
8038
8102
|
'<span class="scan-verdict-badge badge-review">LOOP</span>' +
|
|
8039
8103
|
'<span class="scan-rule-name">' +
|
|
8040
|
-
|
|
8104
|
+
loops.length +
|
|
8041
8105
|
' pattern' +
|
|
8042
|
-
(
|
|
8106
|
+
(loops.length !== 1 ? 's' : '') +
|
|
8043
8107
|
' \xB7 ' +
|
|
8044
8108
|
totalReps +
|
|
8045
8109
|
' total repetitions' +
|
|
@@ -8113,10 +8177,20 @@ var init_ui = __esm({
|
|
|
8113
8177
|
const data = await res.json();
|
|
8114
8178
|
results.style.display = 'block';
|
|
8115
8179
|
|
|
8116
|
-
if (data.status === 'complete') {
|
|
8117
|
-
const
|
|
8118
|
-
const
|
|
8119
|
-
const
|
|
8180
|
+
if (data.status === 'complete' && data.summary) {
|
|
8181
|
+
const summary = data.summary;
|
|
8182
|
+
const byVerdict = summary.byVerdict || {};
|
|
8183
|
+
const sections = summary.sections || [];
|
|
8184
|
+
const leaks = summary.leaks || [];
|
|
8185
|
+
// Flatten rule findings across all sections so we can show a Top-N list
|
|
8186
|
+
const allRisks = [];
|
|
8187
|
+
for (const section of sections) {
|
|
8188
|
+
for (const rule of section.rules || []) {
|
|
8189
|
+
for (const f of rule.findings) {
|
|
8190
|
+
allRisks.push({ rule: rule.name, verdict: rule.verdict, ...f });
|
|
8191
|
+
}
|
|
8192
|
+
}
|
|
8193
|
+
}
|
|
8120
8194
|
|
|
8121
8195
|
let risksHtml = '';
|
|
8122
8196
|
if (allRisks.length > 0) {
|
|
@@ -8129,7 +8203,7 @@ var init_ui = __esm({
|
|
|
8129
8203
|
(r) => \`
|
|
8130
8204
|
<div class="finding-item">
|
|
8131
8205
|
<span class="finding-ts">\${new Date(r.timestamp).toLocaleDateString([], { month: 'short', day: 'numeric' })}</span>
|
|
8132
|
-
<span class="finding-rule">\${r.rule
|
|
8206
|
+
<span class="finding-rule">\${esc(r.rule)}</span>
|
|
8133
8207
|
<span class="finding-cmd"><span class="finding-badge \${r.verdict === 'block' ? 'badge-block' : ''}">\${r.verdict === 'block' ? '\u{1F6D1}' : '\u{1F441}\uFE0F'}</span>\${esc(truncate(r.command, 120))}</span>
|
|
8134
8208
|
</div>
|
|
8135
8209
|
\`
|
|
@@ -8140,18 +8214,18 @@ var init_ui = __esm({
|
|
|
8140
8214
|
}
|
|
8141
8215
|
|
|
8142
8216
|
let leaksHtml = '';
|
|
8143
|
-
if (
|
|
8217
|
+
if (leaks.length > 0) {
|
|
8144
8218
|
leaksHtml = \`
|
|
8145
8219
|
<div class="findings-list" style="border-color: rgba(201, 60, 55, 0.3);">
|
|
8146
8220
|
<div style="font-size: 10px; font-weight: 700; color: #f87171; margin-bottom: 8px; text-transform: uppercase;">Credential Leaks Identified</div>
|
|
8147
|
-
\${
|
|
8221
|
+
\${leaks
|
|
8148
8222
|
.slice(0, 5)
|
|
8149
8223
|
.map(
|
|
8150
8224
|
(l) => \`
|
|
8151
8225
|
<div class="finding-item">
|
|
8152
8226
|
<span class="finding-ts">\${new Date(l.timestamp).toLocaleDateString([], { month: 'short', day: 'numeric' })}</span>
|
|
8153
|
-
<span class="finding-rule">\${esc(l.
|
|
8154
|
-
<span class="finding-cmd"><span class="finding-badge badge-leak">\u{1F511}</span>\${esc(l.
|
|
8227
|
+
<span class="finding-rule">\${esc(l.patternName)}</span>
|
|
8228
|
+
<span class="finding-cmd"><span class="finding-badge badge-leak">\u{1F511}</span>\${esc(l.redactedSample)}</span>
|
|
8155
8229
|
</div>
|
|
8156
8230
|
\`
|
|
8157
8231
|
)
|
|
@@ -8160,10 +8234,11 @@ var init_ui = __esm({
|
|
|
8160
8234
|
\`;
|
|
8161
8235
|
}
|
|
8162
8236
|
|
|
8237
|
+
const riskCount = (byVerdict.blocked || 0) + (byVerdict.supervised || 0);
|
|
8163
8238
|
results.innerHTML = \`
|
|
8164
8239
|
<div class="insight-hint" style="color: #57ab5a; border-color: rgba(87, 171, 90, 0.4); background: rgba(87, 171, 90, 0.08);">
|
|
8165
8240
|
<strong>\u2705 History Audit Complete</strong><br>
|
|
8166
|
-
Scanned \${
|
|
8241
|
+
Scanned \${summary.stats?.sessions || 0} sessions. Found <strong>\${riskCount} risky operations</strong> and <strong>\${byVerdict.leaks || 0} secret leaks</strong>.
|
|
8167
8242
|
Node9 is now actively protecting you from these types of events.
|
|
8168
8243
|
</div>
|
|
8169
8244
|
\${leaksHtml}
|
|
@@ -8249,6 +8324,195 @@ var init_daemon_starter = __esm({
|
|
|
8249
8324
|
}
|
|
8250
8325
|
});
|
|
8251
8326
|
|
|
8327
|
+
// src/scan-summary.ts
|
|
8328
|
+
function buildScanSummary(agents) {
|
|
8329
|
+
const stats = {
|
|
8330
|
+
sessions: 0,
|
|
8331
|
+
totalToolCalls: 0,
|
|
8332
|
+
bashCalls: 0,
|
|
8333
|
+
totalCostUSD: 0,
|
|
8334
|
+
firstDate: null,
|
|
8335
|
+
lastDate: null
|
|
8336
|
+
};
|
|
8337
|
+
for (const a of agents) {
|
|
8338
|
+
stats.sessions += a.scan.sessions;
|
|
8339
|
+
stats.totalToolCalls += a.scan.totalToolCalls;
|
|
8340
|
+
stats.bashCalls += a.scan.bashCalls;
|
|
8341
|
+
stats.totalCostUSD += a.scan.totalCostUSD;
|
|
8342
|
+
if (a.scan.firstDate && (!stats.firstDate || a.scan.firstDate < stats.firstDate)) {
|
|
8343
|
+
stats.firstDate = a.scan.firstDate;
|
|
8344
|
+
}
|
|
8345
|
+
if (a.scan.lastDate && (!stats.lastDate || a.scan.lastDate > stats.lastDate)) {
|
|
8346
|
+
stats.lastDate = a.scan.lastDate;
|
|
8347
|
+
}
|
|
8348
|
+
}
|
|
8349
|
+
const allFindings = agents.flatMap((a) => a.scan.findings);
|
|
8350
|
+
const allLeaks = agents.flatMap(
|
|
8351
|
+
(a) => a.scan.dlpFindings.map((f) => ({
|
|
8352
|
+
patternName: f.patternName,
|
|
8353
|
+
redactedSample: f.redactedSample,
|
|
8354
|
+
toolName: f.toolName,
|
|
8355
|
+
timestamp: f.timestamp,
|
|
8356
|
+
project: f.project,
|
|
8357
|
+
sessionId: f.sessionId,
|
|
8358
|
+
agent: f.agent
|
|
8359
|
+
}))
|
|
8360
|
+
);
|
|
8361
|
+
const allLoops = agents.flatMap(
|
|
8362
|
+
(a) => a.scan.loopFindings.map((f) => ({
|
|
8363
|
+
toolName: f.toolName,
|
|
8364
|
+
commandPreview: f.commandPreview,
|
|
8365
|
+
count: f.count,
|
|
8366
|
+
timestamp: f.timestamp,
|
|
8367
|
+
project: f.project,
|
|
8368
|
+
sessionId: f.sessionId,
|
|
8369
|
+
agent: f.agent
|
|
8370
|
+
}))
|
|
8371
|
+
);
|
|
8372
|
+
const byVerdict = {
|
|
8373
|
+
blocked: allFindings.filter((f) => f.source.rule.verdict === "block").length,
|
|
8374
|
+
supervised: allFindings.filter((f) => f.source.rule.verdict === "review").length,
|
|
8375
|
+
leaks: allLeaks.length,
|
|
8376
|
+
loops: allLoops.length
|
|
8377
|
+
};
|
|
8378
|
+
const byAgent = agents.map((a) => ({
|
|
8379
|
+
id: a.id,
|
|
8380
|
+
label: a.label,
|
|
8381
|
+
icon: a.icon,
|
|
8382
|
+
sessions: a.scan.sessions,
|
|
8383
|
+
findings: a.scan.findings.length + a.scan.dlpFindings.length + a.scan.loopFindings.length,
|
|
8384
|
+
costUSD: a.scan.totalCostUSD
|
|
8385
|
+
})).filter((s) => s.sessions > 0 || s.findings > 0);
|
|
8386
|
+
const sections = buildSections(allFindings);
|
|
8387
|
+
const wastedIters = allLoops.reduce(
|
|
8388
|
+
(sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE),
|
|
8389
|
+
0
|
|
8390
|
+
);
|
|
8391
|
+
const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
|
|
8392
|
+
return {
|
|
8393
|
+
stats,
|
|
8394
|
+
byVerdict,
|
|
8395
|
+
byAgent,
|
|
8396
|
+
sections,
|
|
8397
|
+
leaks: allLeaks,
|
|
8398
|
+
loops: allLoops,
|
|
8399
|
+
loopWastedUSD
|
|
8400
|
+
};
|
|
8401
|
+
}
|
|
8402
|
+
function buildSections(findings) {
|
|
8403
|
+
const sectionMap = /* @__PURE__ */ new Map();
|
|
8404
|
+
function ensureSection(id, label, subtitle, sourceType, shieldKey) {
|
|
8405
|
+
let s = sectionMap.get(id);
|
|
8406
|
+
if (!s) {
|
|
8407
|
+
s = {
|
|
8408
|
+
id,
|
|
8409
|
+
label,
|
|
8410
|
+
subtitle,
|
|
8411
|
+
sourceType,
|
|
8412
|
+
shieldKey,
|
|
8413
|
+
blockedCount: 0,
|
|
8414
|
+
reviewCount: 0,
|
|
8415
|
+
rules: []
|
|
8416
|
+
};
|
|
8417
|
+
sectionMap.set(id, s);
|
|
8418
|
+
}
|
|
8419
|
+
return s;
|
|
8420
|
+
}
|
|
8421
|
+
const ruleMap = /* @__PURE__ */ new Map();
|
|
8422
|
+
for (const f of findings) {
|
|
8423
|
+
const src = f.source;
|
|
8424
|
+
const sourceType = src.sourceType;
|
|
8425
|
+
const shieldName = src.shieldName;
|
|
8426
|
+
const verdict = src.rule.verdict === "block" ? "block" : "review";
|
|
8427
|
+
let sectionId;
|
|
8428
|
+
let sectionLabel;
|
|
8429
|
+
let sectionSubtitle;
|
|
8430
|
+
let shieldKey;
|
|
8431
|
+
if (sourceType === "default") {
|
|
8432
|
+
sectionId = "default";
|
|
8433
|
+
sectionLabel = "Default Rules";
|
|
8434
|
+
sectionSubtitle = "built-in, always on";
|
|
8435
|
+
} else if (sourceType === "shield") {
|
|
8436
|
+
sectionId = `shield:${shieldName}`;
|
|
8437
|
+
sectionLabel = shieldName;
|
|
8438
|
+
sectionSubtitle = SHIELDS[shieldName]?.description ?? "";
|
|
8439
|
+
shieldKey = shieldName;
|
|
8440
|
+
} else if (shieldName === "cloud") {
|
|
8441
|
+
sectionId = "cloud";
|
|
8442
|
+
sectionLabel = "Cloud Policy";
|
|
8443
|
+
sectionSubtitle = "synced from node9 cloud";
|
|
8444
|
+
} else {
|
|
8445
|
+
sectionId = "user";
|
|
8446
|
+
sectionLabel = "Your Rules";
|
|
8447
|
+
sectionSubtitle = "added in node9.config.json";
|
|
8448
|
+
}
|
|
8449
|
+
const section = ensureSection(sectionId, sectionLabel, sectionSubtitle, sourceType, shieldKey);
|
|
8450
|
+
const ruleDisplayName = (src.rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
|
|
8451
|
+
const ruleKey = sectionId + "::" + ruleDisplayName;
|
|
8452
|
+
let rule = ruleMap.get(ruleKey);
|
|
8453
|
+
if (!rule) {
|
|
8454
|
+
rule = {
|
|
8455
|
+
name: ruleDisplayName,
|
|
8456
|
+
verdict,
|
|
8457
|
+
reason: src.rule.reason ?? "",
|
|
8458
|
+
findings: []
|
|
8459
|
+
};
|
|
8460
|
+
ruleMap.set(ruleKey, rule);
|
|
8461
|
+
section.rules.push(rule);
|
|
8462
|
+
}
|
|
8463
|
+
const cmdPreview = previewCommand(f.input, 120);
|
|
8464
|
+
const fullCmd = fullCommandOf(f.input);
|
|
8465
|
+
const isDupe = rule.findings.some((x) => x.project === f.project && x.command === cmdPreview);
|
|
8466
|
+
if (!isDupe) {
|
|
8467
|
+
rule.findings.push({
|
|
8468
|
+
timestamp: f.timestamp ?? "",
|
|
8469
|
+
command: cmdPreview,
|
|
8470
|
+
fullCommand: fullCmd,
|
|
8471
|
+
project: f.project,
|
|
8472
|
+
sessionId: f.sessionId,
|
|
8473
|
+
agent: f.agent,
|
|
8474
|
+
toolName: f.toolName
|
|
8475
|
+
});
|
|
8476
|
+
}
|
|
8477
|
+
if (verdict === "block") section.blockedCount++;
|
|
8478
|
+
else section.reviewCount++;
|
|
8479
|
+
}
|
|
8480
|
+
const sections = [...sectionMap.values()];
|
|
8481
|
+
sections.sort((a, b) => {
|
|
8482
|
+
const aTotal = a.blockedCount + a.reviewCount;
|
|
8483
|
+
const bTotal = b.blockedCount + b.reviewCount;
|
|
8484
|
+
if (b.blockedCount !== a.blockedCount) return b.blockedCount - a.blockedCount;
|
|
8485
|
+
return bTotal - aTotal;
|
|
8486
|
+
});
|
|
8487
|
+
for (const s of sections) {
|
|
8488
|
+
s.rules.sort((a, b) => {
|
|
8489
|
+
const aBlock = a.verdict === "block" ? 1 : 0;
|
|
8490
|
+
const bBlock = b.verdict === "block" ? 1 : 0;
|
|
8491
|
+
if (bBlock !== aBlock) return bBlock - aBlock;
|
|
8492
|
+
return b.findings.length - a.findings.length;
|
|
8493
|
+
});
|
|
8494
|
+
}
|
|
8495
|
+
return sections;
|
|
8496
|
+
}
|
|
8497
|
+
function previewCommand(input, max) {
|
|
8498
|
+
const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
|
|
8499
|
+
const s = String(raw).replace(/\s+/g, " ").trim();
|
|
8500
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
8501
|
+
}
|
|
8502
|
+
function fullCommandOf(input) {
|
|
8503
|
+
const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
|
|
8504
|
+
return String(raw).replace(/\s+/g, " ").trim();
|
|
8505
|
+
}
|
|
8506
|
+
var LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD;
|
|
8507
|
+
var init_scan_summary = __esm({
|
|
8508
|
+
"src/scan-summary.ts"() {
|
|
8509
|
+
"use strict";
|
|
8510
|
+
init_shields();
|
|
8511
|
+
LOOP_THRESHOLD_FOR_WASTE = 3;
|
|
8512
|
+
COST_PER_LOOP_ITER_USD = 6e-3;
|
|
8513
|
+
}
|
|
8514
|
+
});
|
|
8515
|
+
|
|
8252
8516
|
// src/cli/commands/scan.ts
|
|
8253
8517
|
function claudeModelPrice(model) {
|
|
8254
8518
|
const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
|
|
@@ -8289,10 +8553,6 @@ function preview(input, max) {
|
|
|
8289
8553
|
const s = String(cmd).replace(/\s+/g, " ").trim();
|
|
8290
8554
|
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
8291
8555
|
}
|
|
8292
|
-
function fullCommand(input) {
|
|
8293
|
-
const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
|
|
8294
|
-
return String(cmd).replace(/\s+/g, " ").trim();
|
|
8295
|
-
}
|
|
8296
8556
|
function detectLoops(calls, project, sessionId, agent) {
|
|
8297
8557
|
const counts = /* @__PURE__ */ new Map();
|
|
8298
8558
|
for (const call of calls) {
|
|
@@ -9063,26 +9323,32 @@ function printFindingRow(f, drillDown, showSessionId, previewWidth) {
|
|
|
9063
9323
|
const ts = f.timestamp ? import_chalk2.default.dim(fmtTs(f.timestamp) + " ") : "";
|
|
9064
9324
|
const proj = import_chalk2.default.dim(f.project.slice(0, 22).padEnd(22) + " ");
|
|
9065
9325
|
const agentBadge = f.agent === "gemini" ? import_chalk2.default.blue("[Gemini] ") : f.agent === "codex" ? import_chalk2.default.magenta("[Codex] ") : import_chalk2.default.cyan("[Claude] ");
|
|
9066
|
-
|
|
9326
|
+
let cmdText;
|
|
9327
|
+
if (drillDown) {
|
|
9328
|
+
cmdText = f.fullCommand;
|
|
9329
|
+
} else {
|
|
9330
|
+
cmdText = f.command;
|
|
9331
|
+
if (cmdText.length > previewWidth) cmdText = cmdText.slice(0, previewWidth - 1) + "\u2026";
|
|
9332
|
+
}
|
|
9333
|
+
const cmd = import_chalk2.default.gray(cmdText);
|
|
9067
9334
|
const sessionSuffix = showSessionId && f.sessionId ? import_chalk2.default.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
|
|
9068
9335
|
console.log(` ${ts}${proj}${agentBadge}${cmd}${sessionSuffix}`);
|
|
9069
9336
|
}
|
|
9070
|
-
function printRuleGroup(
|
|
9071
|
-
const
|
|
9072
|
-
const ruleCount =
|
|
9337
|
+
function printRuleGroup(rule, topN, drillDown, previewWidth) {
|
|
9338
|
+
const findings = rule.findings;
|
|
9339
|
+
const ruleCount = findings.length;
|
|
9073
9340
|
const countBadge = ruleCount > 1 ? import_chalk2.default.white(` \xD7${ruleCount}`) : "";
|
|
9074
|
-
const
|
|
9075
|
-
const icon = verdictIcon(rule.verdict ?? "review");
|
|
9341
|
+
const icon = verdictIcon(rule.verdict);
|
|
9076
9342
|
console.log(
|
|
9077
|
-
" " + icon + " " + import_chalk2.default.white(
|
|
9343
|
+
" " + icon + " " + import_chalk2.default.white(rule.name) + countBadge + (rule.reason ? import_chalk2.default.dim(` \u2014 ${rule.reason}`) : "")
|
|
9078
9344
|
);
|
|
9079
|
-
const shown = drillDown ?
|
|
9345
|
+
const shown = drillDown ? findings : findings.slice(0, topN);
|
|
9080
9346
|
for (const f of shown) {
|
|
9081
9347
|
printFindingRow(f, drillDown, drillDown, previewWidth);
|
|
9082
9348
|
}
|
|
9083
|
-
if (!drillDown &&
|
|
9349
|
+
if (!drillDown && findings.length > topN) {
|
|
9084
9350
|
console.log(
|
|
9085
|
-
import_chalk2.default.dim(` \u2026 and ${
|
|
9351
|
+
import_chalk2.default.dim(` \u2026 and ${findings.length - topN} more (--drill-down for full list)`)
|
|
9086
9352
|
);
|
|
9087
9353
|
}
|
|
9088
9354
|
}
|
|
@@ -9129,6 +9395,11 @@ function registerScanCommand(program2) {
|
|
|
9129
9395
|
(done) => onProgress(claudeScan.filesScanned + geminiScan.filesScanned + done)
|
|
9130
9396
|
);
|
|
9131
9397
|
const scan = mergeScans(mergeScans(claudeScan, geminiScan), codexScan);
|
|
9398
|
+
const summary = buildScanSummary([
|
|
9399
|
+
{ id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
|
|
9400
|
+
{ id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
|
|
9401
|
+
{ id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
|
|
9402
|
+
]);
|
|
9132
9403
|
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
9133
9404
|
if (scan.filesScanned === 0) {
|
|
9134
9405
|
console.log(import_chalk2.default.yellow(" No session history found."));
|
|
@@ -9191,76 +9462,27 @@ function registerScanCommand(program2) {
|
|
|
9191
9462
|
);
|
|
9192
9463
|
}
|
|
9193
9464
|
console.log("");
|
|
9194
|
-
const sections
|
|
9195
|
-
const defaultFindings = scan.findings.filter((f) => f.source.sourceType === "default");
|
|
9196
|
-
if (defaultFindings.length > 0) {
|
|
9197
|
-
sections.push({
|
|
9198
|
-
label: "Default Rules",
|
|
9199
|
-
subtitle: "built-in, always on",
|
|
9200
|
-
findings: defaultFindings
|
|
9201
|
-
});
|
|
9202
|
-
}
|
|
9203
|
-
const byShield = /* @__PURE__ */ new Map();
|
|
9204
|
-
for (const f of scan.findings.filter((f2) => f2.source.sourceType === "shield")) {
|
|
9205
|
-
const arr = byShield.get(f.source.shieldName) ?? [];
|
|
9206
|
-
arr.push(f);
|
|
9207
|
-
byShield.set(f.source.shieldName, arr);
|
|
9208
|
-
}
|
|
9209
|
-
const shieldsWithFindings = [...byShield.entries()].sort(
|
|
9210
|
-
(a, b) => b[1].length - a[1].length
|
|
9211
|
-
);
|
|
9212
|
-
for (const [shieldName, findings] of shieldsWithFindings) {
|
|
9213
|
-
const description = SHIELDS[shieldName]?.description ?? "";
|
|
9214
|
-
sections.push({
|
|
9215
|
-
label: shieldName,
|
|
9216
|
-
subtitle: description,
|
|
9217
|
-
shieldKey: shieldName,
|
|
9218
|
-
findings
|
|
9219
|
-
});
|
|
9220
|
-
}
|
|
9221
|
-
const userFindings = scan.findings.filter(
|
|
9222
|
-
(f) => f.source.sourceType === "user" || f.source.shieldName === "cloud"
|
|
9223
|
-
);
|
|
9224
|
-
if (userFindings.length > 0) {
|
|
9225
|
-
sections.push({
|
|
9226
|
-
label: "Your Rules",
|
|
9227
|
-
subtitle: "added in node9.config.json",
|
|
9228
|
-
findings: userFindings
|
|
9229
|
-
});
|
|
9230
|
-
}
|
|
9231
|
-
for (const section of sections) {
|
|
9232
|
-
const sectionBlocked = section.findings.filter(
|
|
9233
|
-
(f) => f.source.rule.verdict === "block"
|
|
9234
|
-
).length;
|
|
9235
|
-
const sectionReview = section.findings.length - sectionBlocked;
|
|
9465
|
+
for (const section of summary.sections) {
|
|
9236
9466
|
const countParts = [];
|
|
9237
|
-
if (
|
|
9238
|
-
|
|
9467
|
+
if (section.blockedCount > 0)
|
|
9468
|
+
countParts.push(import_chalk2.default.red(`${section.blockedCount} blocked`));
|
|
9469
|
+
if (section.reviewCount > 0)
|
|
9470
|
+
countParts.push(import_chalk2.default.yellow(`${section.reviewCount} review`));
|
|
9239
9471
|
const countStr = countParts.join(import_chalk2.default.dim(" \xB7 "));
|
|
9240
9472
|
const enableHint = section.shieldKey ? import_chalk2.default.dim(` \u2192 node9 shield enable ${section.shieldKey}`) : "";
|
|
9241
9473
|
console.log(" " + import_chalk2.default.dim("\u2500".repeat(70)));
|
|
9242
9474
|
console.log(
|
|
9243
9475
|
" " + import_chalk2.default.bold(section.label) + (section.subtitle ? import_chalk2.default.dim(` \xB7 ${section.subtitle}`) : "") + " " + countStr + enableHint
|
|
9244
9476
|
);
|
|
9245
|
-
const
|
|
9246
|
-
|
|
9247
|
-
const ruleKey = f.source.rule.name ?? "unnamed";
|
|
9248
|
-
const arr = byRule.get(ruleKey) ?? [];
|
|
9249
|
-
arr.push(f);
|
|
9250
|
-
byRule.set(ruleKey, arr);
|
|
9251
|
-
}
|
|
9252
|
-
const sortedRules = [...byRule.entries()].sort((a, b) => {
|
|
9253
|
-
const aBlock = a[1][0].source.rule.verdict === "block" ? 1 : 0;
|
|
9254
|
-
const bBlock = b[1][0].source.rule.verdict === "block" ? 1 : 0;
|
|
9255
|
-
if (bBlock !== aBlock) return bBlock - aBlock;
|
|
9256
|
-
return b[1].length - a[1].length;
|
|
9257
|
-
});
|
|
9258
|
-
for (const [, ruleFindings] of sortedRules) {
|
|
9259
|
-
printRuleGroup(ruleFindings, topN, drillDown, previewWidth);
|
|
9477
|
+
for (const rule of section.rules) {
|
|
9478
|
+
printRuleGroup(rule, topN, drillDown, previewWidth);
|
|
9260
9479
|
}
|
|
9261
9480
|
console.log("");
|
|
9262
9481
|
}
|
|
9263
|
-
const
|
|
9482
|
+
const activeShieldIds = new Set(
|
|
9483
|
+
summary.sections.filter((s) => s.sourceType === "shield" && s.shieldKey).map((s) => s.shieldKey)
|
|
9484
|
+
);
|
|
9485
|
+
const emptyShields = Object.keys(SHIELDS).filter((n) => !activeShieldIds.has(n)).sort();
|
|
9264
9486
|
if (emptyShields.length > 0) {
|
|
9265
9487
|
console.log(" " + import_chalk2.default.dim("\u2500".repeat(70)));
|
|
9266
9488
|
console.log(
|
|
@@ -9370,78 +9592,12 @@ function registerScanCommand(program2) {
|
|
|
9370
9592
|
const internalToken = getInternalToken();
|
|
9371
9593
|
if (internalToken) {
|
|
9372
9594
|
try {
|
|
9373
|
-
const
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
verdict: f.source?.rule?.verdict ?? f.source?.rule?.action ?? "review",
|
|
9380
|
-
ruleSource: f.source?.sourceType ?? "default",
|
|
9381
|
-
source: src
|
|
9382
|
-
}))
|
|
9383
|
-
);
|
|
9384
|
-
const mapLeaks = (arr, src) => (
|
|
9385
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9386
|
-
arr.map((f) => ({
|
|
9387
|
-
timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
9388
|
-
pattern: f.patternName || "DLP",
|
|
9389
|
-
sample: f.redactedSample || "********",
|
|
9390
|
-
source: src
|
|
9391
|
-
}))
|
|
9392
|
-
);
|
|
9393
|
-
const mapLoops = (arr, src) => (
|
|
9394
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9395
|
-
arr.map((f) => ({
|
|
9396
|
-
timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
9397
|
-
toolName: f.toolName || "unknown",
|
|
9398
|
-
commandPreview: f.commandPreview || "",
|
|
9399
|
-
count: f.count || 0,
|
|
9400
|
-
source: src
|
|
9401
|
-
}))
|
|
9402
|
-
);
|
|
9403
|
-
const sources = [
|
|
9404
|
-
{
|
|
9405
|
-
id: "claude",
|
|
9406
|
-
label: "Claude",
|
|
9407
|
-
icon: "\u{1F916}",
|
|
9408
|
-
sessions: claudeScan.sessions,
|
|
9409
|
-
findings: mapFindings(claudeScan.findings, "claude"),
|
|
9410
|
-
leaks: mapLeaks(claudeScan.dlpFindings, "claude"),
|
|
9411
|
-
loops: mapLoops(claudeScan.loopFindings, "claude")
|
|
9412
|
-
},
|
|
9413
|
-
{
|
|
9414
|
-
id: "gemini",
|
|
9415
|
-
label: "Gemini",
|
|
9416
|
-
icon: "\u264A",
|
|
9417
|
-
sessions: geminiScan.sessions,
|
|
9418
|
-
findings: mapFindings(geminiScan.findings, "gemini"),
|
|
9419
|
-
leaks: mapLeaks(geminiScan.dlpFindings, "gemini"),
|
|
9420
|
-
loops: mapLoops(geminiScan.loopFindings, "gemini")
|
|
9421
|
-
},
|
|
9422
|
-
{
|
|
9423
|
-
id: "codex",
|
|
9424
|
-
label: "Codex",
|
|
9425
|
-
icon: "\u{1F52E}",
|
|
9426
|
-
sessions: codexScan.sessions,
|
|
9427
|
-
findings: mapFindings(codexScan.findings, "codex"),
|
|
9428
|
-
leaks: mapLeaks(codexScan.dlpFindings, "codex"),
|
|
9429
|
-
loops: mapLoops(codexScan.loopFindings, "codex")
|
|
9430
|
-
}
|
|
9431
|
-
].filter(
|
|
9432
|
-
(s) => s.sessions > 0 || s.findings.length > 0 || s.leaks.length > 0 || s.loops.length > 0
|
|
9433
|
-
);
|
|
9434
|
-
const payload = {
|
|
9435
|
-
status: "complete",
|
|
9436
|
-
summary: {
|
|
9437
|
-
sessions: scan.sessions,
|
|
9438
|
-
findings: scan.findings.length,
|
|
9439
|
-
dlp: scan.dlpFindings.length,
|
|
9440
|
-
loops: scan.loopFindings.length,
|
|
9441
|
-
totalCostUSD: scan.totalCostUSD
|
|
9442
|
-
},
|
|
9443
|
-
sources
|
|
9444
|
-
};
|
|
9595
|
+
const summary2 = buildScanSummary([
|
|
9596
|
+
{ id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
|
|
9597
|
+
{ id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
|
|
9598
|
+
{ id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
|
|
9599
|
+
]);
|
|
9600
|
+
const payload = { status: "complete", summary: summary2 };
|
|
9445
9601
|
await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/scan/push`, {
|
|
9446
9602
|
method: "POST",
|
|
9447
9603
|
headers: { "Content-Type": "application/json", "x-node9-internal": internalToken },
|
|
@@ -9472,6 +9628,7 @@ var init_scan = __esm({
|
|
|
9472
9628
|
init_dlp();
|
|
9473
9629
|
init_daemon();
|
|
9474
9630
|
init_daemon_starter();
|
|
9631
|
+
init_scan_summary();
|
|
9475
9632
|
CLAUDE_PRICING = {
|
|
9476
9633
|
"claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
9477
9634
|
"claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
@@ -11448,9 +11605,21 @@ data: ${JSON.stringify(item.data)}
|
|
|
11448
11605
|
const d = /* @__PURE__ */ new Date();
|
|
11449
11606
|
d.setDate(d.getDate() - 90);
|
|
11450
11607
|
d.setHours(0, 0, 0, 0);
|
|
11451
|
-
|
|
11452
|
-
|
|
11453
|
-
|
|
11608
|
+
const EMPTY_SCAN = {
|
|
11609
|
+
filesScanned: 0,
|
|
11610
|
+
sessions: 0,
|
|
11611
|
+
totalToolCalls: 0,
|
|
11612
|
+
bashCalls: 0,
|
|
11613
|
+
findings: [],
|
|
11614
|
+
dlpFindings: [],
|
|
11615
|
+
loopFindings: [],
|
|
11616
|
+
totalCostUSD: 0,
|
|
11617
|
+
firstDate: null,
|
|
11618
|
+
lastDate: null
|
|
11619
|
+
};
|
|
11620
|
+
let claude = EMPTY_SCAN;
|
|
11621
|
+
let gemini = EMPTY_SCAN;
|
|
11622
|
+
let codex = EMPTY_SCAN;
|
|
11454
11623
|
try {
|
|
11455
11624
|
claude = scanClaudeHistory(d);
|
|
11456
11625
|
} catch (e) {
|
|
@@ -11466,86 +11635,13 @@ data: ${JSON.stringify(item.data)}
|
|
|
11466
11635
|
} catch (e) {
|
|
11467
11636
|
console.error("Codex scan failed:", e);
|
|
11468
11637
|
}
|
|
11469
|
-
const
|
|
11470
|
-
|
|
11471
|
-
|
|
11472
|
-
|
|
11473
|
-
|
|
11474
|
-
command: f.input?.command ?? f.input?.cmd ?? f.input?.file_path ?? f.toolName ?? "unknown",
|
|
11475
|
-
verdict: f.source?.rule?.verdict ?? f.source?.rule?.action ?? "review",
|
|
11476
|
-
ruleSource: f.source?.sourceType ?? "default",
|
|
11477
|
-
source: src
|
|
11478
|
-
}))
|
|
11479
|
-
);
|
|
11480
|
-
const mapLeaks = (arr, src) => (
|
|
11481
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11482
|
-
arr.map((f) => ({
|
|
11483
|
-
timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
11484
|
-
pattern: f.patternName || "DLP",
|
|
11485
|
-
sample: f.redactedSample || "********",
|
|
11486
|
-
source: src
|
|
11487
|
-
}))
|
|
11488
|
-
);
|
|
11489
|
-
const mapLoops = (arr, src) => (
|
|
11490
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11491
|
-
arr.map((f) => ({
|
|
11492
|
-
timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
11493
|
-
toolName: f.toolName || "unknown",
|
|
11494
|
-
commandPreview: f.commandPreview || "",
|
|
11495
|
-
count: f.count || 0,
|
|
11496
|
-
source: src
|
|
11497
|
-
}))
|
|
11498
|
-
);
|
|
11499
|
-
const sources = [
|
|
11500
|
-
{
|
|
11501
|
-
id: "claude",
|
|
11502
|
-
label: "Claude",
|
|
11503
|
-
icon: "\u{1F916}",
|
|
11504
|
-
sessions: claude.sessions,
|
|
11505
|
-
findings: mapFindings(claude.findings, "claude"),
|
|
11506
|
-
leaks: mapLeaks(claude.dlpFindings, "claude"),
|
|
11507
|
-
loops: mapLoops(claude.loopFindings, "claude")
|
|
11508
|
-
},
|
|
11509
|
-
{
|
|
11510
|
-
id: "gemini",
|
|
11511
|
-
label: "Gemini",
|
|
11512
|
-
icon: "\u264A",
|
|
11513
|
-
sessions: gemini.sessions,
|
|
11514
|
-
findings: mapFindings(gemini.findings, "gemini"),
|
|
11515
|
-
leaks: mapLeaks(gemini.dlpFindings, "gemini"),
|
|
11516
|
-
loops: mapLoops(gemini.loopFindings, "gemini")
|
|
11517
|
-
},
|
|
11518
|
-
{
|
|
11519
|
-
id: "codex",
|
|
11520
|
-
label: "Codex",
|
|
11521
|
-
icon: "\u{1F52E}",
|
|
11522
|
-
sessions: codex.sessions,
|
|
11523
|
-
findings: mapFindings(codex.findings, "codex"),
|
|
11524
|
-
leaks: mapLeaks(codex.dlpFindings, "codex"),
|
|
11525
|
-
loops: mapLoops(codex.loopFindings, "codex")
|
|
11526
|
-
}
|
|
11527
|
-
].filter(
|
|
11528
|
-
(s) => s.sessions > 0 || s.findings.length > 0 || s.leaks.length > 0 || s.loops.length > 0
|
|
11529
|
-
);
|
|
11530
|
-
const totalSessions = claude.sessions + gemini.sessions + codex.sessions;
|
|
11531
|
-
const totalFindings = claude.findings.length + gemini.findings.length + codex.findings.length;
|
|
11532
|
-
const totalDlp = claude.dlpFindings.length + gemini.dlpFindings.length + codex.dlpFindings.length;
|
|
11533
|
-
const totalLoops = claude.loopFindings.length + gemini.loopFindings.length + codex.loopFindings.length;
|
|
11534
|
-
const totalCostUSD = (claude.totalCostUSD || 0) + (gemini.totalCostUSD || 0) + (codex.totalCostUSD || 0);
|
|
11638
|
+
const summary = buildScanSummary([
|
|
11639
|
+
{ id: "claude", label: "Claude", icon: "\u{1F916}", scan: claude },
|
|
11640
|
+
{ id: "gemini", label: "Gemini", icon: "\u264A", scan: gemini },
|
|
11641
|
+
{ id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codex }
|
|
11642
|
+
]);
|
|
11535
11643
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
11536
|
-
return res.end(
|
|
11537
|
-
JSON.stringify({
|
|
11538
|
-
status: "complete",
|
|
11539
|
-
summary: {
|
|
11540
|
-
sessions: totalSessions,
|
|
11541
|
-
totalCostUSD,
|
|
11542
|
-
findings: totalFindings,
|
|
11543
|
-
dlp: totalDlp,
|
|
11544
|
-
loops: totalLoops
|
|
11545
|
-
},
|
|
11546
|
-
sources
|
|
11547
|
-
})
|
|
11548
|
-
);
|
|
11644
|
+
return res.end(JSON.stringify({ status: "complete", summary }));
|
|
11549
11645
|
} catch (err2) {
|
|
11550
11646
|
console.error("Scan failed:", err2);
|
|
11551
11647
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -11892,6 +11988,7 @@ var init_server = __esm({
|
|
|
11892
11988
|
init_shields();
|
|
11893
11989
|
init_ui2();
|
|
11894
11990
|
init_scan();
|
|
11991
|
+
init_scan_summary();
|
|
11895
11992
|
init_state2();
|
|
11896
11993
|
init_state();
|
|
11897
11994
|
init_patch();
|
|
@@ -20546,10 +20643,10 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
20546
20643
|
program.help();
|
|
20547
20644
|
return;
|
|
20548
20645
|
}
|
|
20549
|
-
const
|
|
20646
|
+
const fullCommand = runArgs.join(" ");
|
|
20550
20647
|
let result = await authorizeHeadless(
|
|
20551
20648
|
"shell",
|
|
20552
|
-
{ command:
|
|
20649
|
+
{ command: fullCommand },
|
|
20553
20650
|
{
|
|
20554
20651
|
agent: "Terminal"
|
|
20555
20652
|
}
|
|
@@ -20557,11 +20654,11 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
20557
20654
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
20558
20655
|
console.error(import_chalk26.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
20559
20656
|
const daemonReady = await autoStartDaemonAndWait();
|
|
20560
|
-
if (daemonReady) result = await authorizeHeadless("shell", { command:
|
|
20657
|
+
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
20561
20658
|
}
|
|
20562
20659
|
if (result.noApprovalMechanism && process.stdout.isTTY) {
|
|
20563
20660
|
const approved = await (0, import_prompts2.confirm)({
|
|
20564
|
-
message: `\u{1F6E1}\uFE0F Node9: Allow "${
|
|
20661
|
+
message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand}"?`,
|
|
20565
20662
|
default: false
|
|
20566
20663
|
});
|
|
20567
20664
|
result = { approved, reason: approved ? void 0 : "Denied by user at terminal." };
|
|
@@ -20574,7 +20671,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
20574
20671
|
process.exit(1);
|
|
20575
20672
|
}
|
|
20576
20673
|
console.error(import_chalk26.default.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
20577
|
-
await runProxy(
|
|
20674
|
+
await runProxy(fullCommand);
|
|
20578
20675
|
} else {
|
|
20579
20676
|
program.help();
|
|
20580
20677
|
}
|