@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.mjs
CHANGED
|
@@ -5712,6 +5712,12 @@ var init_ui = __esm({
|
|
|
5712
5712
|
text-align: center;
|
|
5713
5713
|
}
|
|
5714
5714
|
/* results */
|
|
5715
|
+
.scan-date-range {
|
|
5716
|
+
text-align: center;
|
|
5717
|
+
font-size: 11px;
|
|
5718
|
+
color: var(--muted);
|
|
5719
|
+
padding: 6px 0 10px;
|
|
5720
|
+
}
|
|
5715
5721
|
.scan-summary-row {
|
|
5716
5722
|
display: grid;
|
|
5717
5723
|
grid-template-columns: repeat(4, 1fr);
|
|
@@ -7604,13 +7610,10 @@ var init_ui = __esm({
|
|
|
7604
7610
|
const res = await fetch('/scan', { headers: { 'X-Node9-Token': CSRF_TOKEN } });
|
|
7605
7611
|
if (!res.ok) return;
|
|
7606
7612
|
const data = await res.json();
|
|
7607
|
-
if (data.status !== 'complete') return;
|
|
7613
|
+
if (data.status !== 'complete' || !data.summary) return;
|
|
7608
7614
|
_scanData = data;
|
|
7609
|
-
const
|
|
7610
|
-
const total =
|
|
7611
|
-
sources.reduce((n, s) => n + (s.findings || []).length, 0) +
|
|
7612
|
-
sources.reduce((n, s) => n + (s.leaks || []).length, 0) +
|
|
7613
|
-
sources.reduce((n, s) => n + (s.loops || []).length, 0);
|
|
7615
|
+
const v = data.summary.byVerdict || {};
|
|
7616
|
+
const total = (v.blocked || 0) + (v.supervised || 0) + (v.leaks || 0) + (v.loops || 0);
|
|
7614
7617
|
if (total > 0) {
|
|
7615
7618
|
const btn = document.getElementById('btn-scan-history');
|
|
7616
7619
|
if (btn && !btn.querySelector('.scan-badge')) {
|
|
@@ -7715,42 +7718,26 @@ var init_ui = __esm({
|
|
|
7715
7718
|
}
|
|
7716
7719
|
|
|
7717
7720
|
function renderScanResults(data) {
|
|
7718
|
-
if (!data || data.status !== 'complete') {
|
|
7721
|
+
if (!data || data.status !== 'complete' || !data.summary) {
|
|
7719
7722
|
return '<div style="text-align:center;color:var(--muted);padding:32px">No data returned.</div>';
|
|
7720
7723
|
}
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
const
|
|
7726
|
-
const
|
|
7727
|
-
const
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
const
|
|
7731
|
-
const
|
|
7732
|
-
(f) => f.verdict !== 'block' && f.ruleSource !== 'user'
|
|
7733
|
-
);
|
|
7734
|
-
const yourRules = allFindings.filter((f) => f.ruleSource === 'user');
|
|
7735
|
-
|
|
7736
|
-
// Loop cost estimate: each wasted iteration ~2K tokens at Sonnet pricing
|
|
7737
|
-
const LOOP_THRESHOLD = 3;
|
|
7738
|
-
const COST_PER_ITER = 0.006;
|
|
7739
|
-
const wastedIters = allLoops.reduce(
|
|
7740
|
-
(sum, l) => sum + Math.max(0, (l.count || 0) - LOOP_THRESHOLD),
|
|
7741
|
-
0
|
|
7742
|
-
);
|
|
7743
|
-
const loopWastedUSD = wastedIters * COST_PER_ITER;
|
|
7744
|
-
|
|
7745
|
-
const totalCost = s.totalCostUSD || 0;
|
|
7724
|
+
// Server-computed ScanSummary (see src/scan-summary.ts).
|
|
7725
|
+
// This renderer is a pure presentation layer \u2014 no categorization here.
|
|
7726
|
+
const summary = data.summary;
|
|
7727
|
+
const stats = summary.stats || {};
|
|
7728
|
+
const byVerdict = summary.byVerdict || { blocked: 0, supervised: 0, leaks: 0, loops: 0 };
|
|
7729
|
+
const byAgent = summary.byAgent || [];
|
|
7730
|
+
const sections = summary.sections || [];
|
|
7731
|
+
const leaks = summary.leaks || [];
|
|
7732
|
+
const loops = summary.loops || [];
|
|
7733
|
+
const loopWastedUSD = summary.loopWastedUSD || 0;
|
|
7734
|
+
const totalCost = stats.totalCostUSD || 0;
|
|
7746
7735
|
|
|
7747
7736
|
function fmtUSD(n) {
|
|
7748
7737
|
if (!n || n < 0.001) return null;
|
|
7749
7738
|
if (n < 1) return '$' + n.toFixed(3);
|
|
7750
7739
|
return '$' + n.toFixed(2);
|
|
7751
7740
|
}
|
|
7752
|
-
|
|
7753
|
-
// \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
|
|
7754
7741
|
const costStr = fmtUSD(totalCost);
|
|
7755
7742
|
const savingsStr = fmtUSD(loopWastedUSD);
|
|
7756
7743
|
|
|
@@ -7772,10 +7759,46 @@ var init_ui = __esm({
|
|
|
7772
7759
|
);
|
|
7773
7760
|
}
|
|
7774
7761
|
|
|
7775
|
-
|
|
7762
|
+
// \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
|
|
7763
|
+
function fmtShort(iso) {
|
|
7764
|
+
if (!iso) return null;
|
|
7765
|
+
try {
|
|
7766
|
+
return new Date(iso).toLocaleDateString([], {
|
|
7767
|
+
month: 'short',
|
|
7768
|
+
day: 'numeric',
|
|
7769
|
+
year: 'numeric',
|
|
7770
|
+
});
|
|
7771
|
+
} catch {
|
|
7772
|
+
return null;
|
|
7773
|
+
}
|
|
7774
|
+
}
|
|
7775
|
+
const firstLabel = fmtShort(stats.firstDate);
|
|
7776
|
+
const lastLabel = fmtShort(stats.lastDate);
|
|
7777
|
+
let daySpan = '';
|
|
7778
|
+
if (stats.firstDate && stats.lastDate) {
|
|
7779
|
+
const days =
|
|
7780
|
+
Math.round(
|
|
7781
|
+
(new Date(stats.lastDate).getTime() - new Date(stats.firstDate).getTime()) /
|
|
7782
|
+
(1000 * 60 * 60 * 24)
|
|
7783
|
+
) + 1;
|
|
7784
|
+
if (days > 1) daySpan = ' \xB7 ' + days + ' day' + (days !== 1 ? 's' : '');
|
|
7785
|
+
}
|
|
7786
|
+
const dateRangeHtml =
|
|
7787
|
+
firstLabel && lastLabel
|
|
7788
|
+
? '<div class="scan-date-range">\u{1F4C5} ' +
|
|
7789
|
+
esc(firstLabel) +
|
|
7790
|
+
' \u2013 ' +
|
|
7791
|
+
esc(lastLabel) +
|
|
7792
|
+
esc(daySpan) +
|
|
7793
|
+
'</div>'
|
|
7794
|
+
: '';
|
|
7795
|
+
|
|
7796
|
+
// \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
|
|
7797
|
+
const statsHtml =
|
|
7798
|
+
dateRangeHtml +
|
|
7776
7799
|
'<div class="scan-summary-row">' +
|
|
7777
7800
|
'<div class="scan-stat">' +
|
|
7778
|
-
statVal(
|
|
7801
|
+
statVal(stats.sessions || 0, '') +
|
|
7779
7802
|
'<div class="scan-stat-label">Sessions</div></div>' +
|
|
7780
7803
|
(totalCost > 0
|
|
7781
7804
|
? '<div class="scan-stat">' +
|
|
@@ -7783,19 +7806,16 @@ var init_ui = __esm({
|
|
|
7783
7806
|
'<div class="scan-stat-label">AI Spend</div></div>'
|
|
7784
7807
|
: '') +
|
|
7785
7808
|
'<div class="scan-stat">' +
|
|
7786
|
-
statVal(blocked
|
|
7809
|
+
statVal(byVerdict.blocked || 0, byVerdict.blocked ? 'danger' : 'ok') +
|
|
7787
7810
|
'<div class="scan-stat-label">Blocked</div></div>' +
|
|
7788
7811
|
'<div class="scan-stat">' +
|
|
7789
|
-
statVal(supervised
|
|
7812
|
+
statVal(byVerdict.supervised || 0, byVerdict.supervised ? 'warning' : 'ok') +
|
|
7790
7813
|
'<div class="scan-stat-label">Supervised</div></div>' +
|
|
7791
7814
|
'<div class="scan-stat">' +
|
|
7792
|
-
statVal(
|
|
7793
|
-
'<div class="scan-stat-label">Your Rules</div></div>' +
|
|
7794
|
-
'<div class="scan-stat">' +
|
|
7795
|
-
statVal(allLeaks.length, allLeaks.length ? 'danger' : 'ok') +
|
|
7815
|
+
statVal(byVerdict.leaks || 0, byVerdict.leaks ? 'danger' : 'ok') +
|
|
7796
7816
|
'<div class="scan-stat-label">Leaks</div></div>' +
|
|
7797
7817
|
'<div class="scan-stat">' +
|
|
7798
|
-
statVal(
|
|
7818
|
+
statVal(byVerdict.loops || 0, byVerdict.loops ? 'warning' : 'ok') +
|
|
7799
7819
|
'<div class="scan-stat-label">Loops</div></div>' +
|
|
7800
7820
|
'</div>' +
|
|
7801
7821
|
(savingsStr
|
|
@@ -7804,56 +7824,103 @@ var init_ui = __esm({
|
|
|
7804
7824
|
'</span> in unnecessary LLM turns</div>'
|
|
7805
7825
|
: '');
|
|
7806
7826
|
|
|
7807
|
-
|
|
7827
|
+
const hasNothing = !sections.length && !leaks.length && !loops.length;
|
|
7828
|
+
if (hasNothing) {
|
|
7808
7829
|
return (
|
|
7809
|
-
|
|
7830
|
+
statsHtml +
|
|
7810
7831
|
'<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 ' +
|
|
7811
|
-
(
|
|
7812
|
-
' sessions
|
|
7813
|
-
sources.length +
|
|
7814
|
-
' AI source(s)</span></div>'
|
|
7832
|
+
(stats.sessions || 0) +
|
|
7833
|
+
' sessions</span></div>'
|
|
7815
7834
|
);
|
|
7816
7835
|
}
|
|
7817
7836
|
|
|
7818
|
-
// \u2500\u2500
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7837
|
+
// \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
|
|
7838
|
+
const agentBreakdown = byAgent.length
|
|
7839
|
+
? '<div class="scan-agent-group">' +
|
|
7840
|
+
byAgent
|
|
7841
|
+
.map(
|
|
7842
|
+
(a) =>
|
|
7843
|
+
'<div class="scan-agent-row">' +
|
|
7844
|
+
'<span class="scan-agent-name">' +
|
|
7845
|
+
esc(a.icon || '') +
|
|
7846
|
+
' ' +
|
|
7847
|
+
esc(a.label) +
|
|
7848
|
+
'</span>' +
|
|
7849
|
+
'<span class="scan-agent-stat">' +
|
|
7850
|
+
a.sessions +
|
|
7851
|
+
' session' +
|
|
7852
|
+
(a.sessions !== 1 ? 's' : '') +
|
|
7853
|
+
'</span>' +
|
|
7854
|
+
'<span class="scan-agent-sep">\xB7</span>' +
|
|
7855
|
+
'<span class="scan-agent-stat">' +
|
|
7856
|
+
a.findings +
|
|
7857
|
+
' finding' +
|
|
7858
|
+
(a.findings !== 1 ? 's' : '') +
|
|
7859
|
+
'</span>' +
|
|
7860
|
+
'</div>'
|
|
7861
|
+
)
|
|
7862
|
+
.join('') +
|
|
7863
|
+
'</div>'
|
|
7864
|
+
: '';
|
|
7865
|
+
|
|
7866
|
+
// \u2500\u2500 Rule row renderer \u2014 shared between rule sections + leaks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7867
|
+
// Determines bar-max across all sections so relative counts stay honest.
|
|
7868
|
+
let maxBar = 1;
|
|
7869
|
+
for (const section of sections) {
|
|
7870
|
+
for (const rule of section.rules || []) {
|
|
7871
|
+
if (rule.findings.length > maxBar) maxBar = rule.findings.length;
|
|
7825
7872
|
}
|
|
7826
|
-
return [...map.entries()].sort((a, b) => b[1].length - a[1].length);
|
|
7827
7873
|
}
|
|
7874
|
+
// Group leaks by patternName for display
|
|
7875
|
+
const leaksByPattern = (function () {
|
|
7876
|
+
const m = new Map();
|
|
7877
|
+
for (const l of leaks) {
|
|
7878
|
+
const k = l.patternName || 'DLP';
|
|
7879
|
+
if (!m.has(k)) m.set(k, []);
|
|
7880
|
+
m.get(k).push(l);
|
|
7881
|
+
}
|
|
7882
|
+
for (const [, group] of m) if (group.length > maxBar) maxBar = group.length;
|
|
7883
|
+
return [...m.entries()].sort((a, b) => b[1].length - a[1].length);
|
|
7884
|
+
})();
|
|
7828
7885
|
|
|
7829
|
-
function findingRow(
|
|
7830
|
-
const date = new Date(
|
|
7886
|
+
function findingRow(timestamp, text) {
|
|
7887
|
+
const date = new Date(timestamp).toLocaleDateString([], {
|
|
7831
7888
|
month: 'short',
|
|
7832
7889
|
day: 'numeric',
|
|
7833
7890
|
});
|
|
7834
|
-
const cmdText =
|
|
7835
|
-
type === 'leak' ? esc(item.sample || '') : esc(truncate(item.command || '', 120));
|
|
7836
7891
|
return (
|
|
7837
7892
|
'<div class="scan-finding-row"><span class="scan-finding-ts">' +
|
|
7838
7893
|
date +
|
|
7839
7894
|
'</span><span class="scan-finding-cmd">' +
|
|
7840
|
-
|
|
7895
|
+
text +
|
|
7841
7896
|
'</span></div>'
|
|
7842
7897
|
);
|
|
7843
7898
|
}
|
|
7844
7899
|
|
|
7845
|
-
function
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
: (i) => (i.rule || 'unknown').replace(/^shield:[^:]+:/, '');
|
|
7850
|
-
const grouped = groupByKey(items, keyFn);
|
|
7851
|
-
return grouped
|
|
7852
|
-
.map(([rule, group]) => {
|
|
7853
|
-
const count = group.length;
|
|
7900
|
+
function ruleCardsForSection(section) {
|
|
7901
|
+
return (section.rules || [])
|
|
7902
|
+
.map((rule) => {
|
|
7903
|
+
const count = rule.findings.length;
|
|
7854
7904
|
const barPct = Math.round((count / maxBar) * 100);
|
|
7855
7905
|
const detailId = 'detail-' + Math.random().toString(36).slice(2);
|
|
7856
|
-
const
|
|
7906
|
+
const badgeClass =
|
|
7907
|
+
section.sourceType === 'user'
|
|
7908
|
+
? 'badge-user'
|
|
7909
|
+
: rule.verdict === 'block'
|
|
7910
|
+
? 'badge-block'
|
|
7911
|
+
: 'badge-review';
|
|
7912
|
+
const badgeLabel =
|
|
7913
|
+
section.sourceType === 'user'
|
|
7914
|
+
? rule.verdict === 'block'
|
|
7915
|
+
? 'YOUR BLOCK'
|
|
7916
|
+
: 'YOUR RULE'
|
|
7917
|
+
: rule.verdict === 'block'
|
|
7918
|
+
? 'BLOCK'
|
|
7919
|
+
: 'REVIEW';
|
|
7920
|
+
const barColor = section.sourceType === 'user' ? ';background:#388bfd33' : '';
|
|
7921
|
+
const rows = rule.findings
|
|
7922
|
+
.map((f) => findingRow(f.timestamp, esc(truncate(f.command || '', 120))))
|
|
7923
|
+
.join('');
|
|
7857
7924
|
return (
|
|
7858
7925
|
'<div class="scan-rule-row" onclick="var d=document.getElementById(\\'' +
|
|
7859
7926
|
detailId +
|
|
@@ -7864,12 +7931,12 @@ var init_ui = __esm({
|
|
|
7864
7931
|
badgeLabel +
|
|
7865
7932
|
'</span>' +
|
|
7866
7933
|
'<span class="scan-rule-name">' +
|
|
7867
|
-
esc(rule) +
|
|
7934
|
+
esc(rule.name) +
|
|
7868
7935
|
'</span>' +
|
|
7869
7936
|
'<span class="scan-rule-bar-wrap"><span class="scan-rule-bar" style="width:' +
|
|
7870
7937
|
barPct +
|
|
7871
7938
|
'%' +
|
|
7872
|
-
|
|
7939
|
+
barColor +
|
|
7873
7940
|
'"></span></span>' +
|
|
7874
7941
|
'<span class="scan-rule-count">' +
|
|
7875
7942
|
count +
|
|
@@ -7885,98 +7952,95 @@ var init_ui = __esm({
|
|
|
7885
7952
|
.join('');
|
|
7886
7953
|
}
|
|
7887
7954
|
|
|
7888
|
-
// \u2500\u2500
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
)
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7955
|
+
// \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
|
|
7956
|
+
function sectionHeading(section) {
|
|
7957
|
+
if (section.id === 'default') {
|
|
7958
|
+
return '\u{1F441} ' + esc(section.label) + ' \u2014 built-in, always on';
|
|
7959
|
+
}
|
|
7960
|
+
if (section.sourceType === 'shield') {
|
|
7961
|
+
return (
|
|
7962
|
+
'\u{1F6E1} ' +
|
|
7963
|
+
esc(section.label) +
|
|
7964
|
+
' shield' +
|
|
7965
|
+
(section.subtitle ? ' \u2014 ' + esc(section.subtitle) : '')
|
|
7966
|
+
);
|
|
7967
|
+
}
|
|
7968
|
+
if (section.id === 'cloud') {
|
|
7969
|
+
return '\u2601 Cloud Policy \u2014 synced from node9';
|
|
7970
|
+
}
|
|
7971
|
+
return '\u2705 Your Rules \u2014 working as configured';
|
|
7972
|
+
}
|
|
7973
|
+
function sectionColor(section) {
|
|
7974
|
+
if (section.blockedCount > 0) return '#e5534b';
|
|
7975
|
+
if (section.sourceType === 'user') return '#58a6ff';
|
|
7976
|
+
return 'var(--muted)';
|
|
7977
|
+
}
|
|
7897
7978
|
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
const
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
const findingsTotal = f + l + lp;
|
|
7979
|
+
let html = statsHtml + agentBreakdown;
|
|
7980
|
+
|
|
7981
|
+
// \u2500\u2500 Leaks (shown first: credential exposure is highest-severity) \u2500\u2500\u2500\u2500\u2500
|
|
7982
|
+
if (leaksByPattern.length) {
|
|
7983
|
+
html +=
|
|
7984
|
+
'<div class="scan-rule-section-label" style="color:#e5534b">\u{1F511} Credential Leaks \u2014 secrets found in tool calls</div>';
|
|
7985
|
+
html += leaksByPattern
|
|
7986
|
+
.map(([pattern, group]) => {
|
|
7987
|
+
const count = group.length;
|
|
7988
|
+
const barPct = Math.round((count / maxBar) * 100);
|
|
7989
|
+
const detailId = 'detail-' + Math.random().toString(36).slice(2);
|
|
7990
|
+
const rows = group
|
|
7991
|
+
.map((l) => findingRow(l.timestamp, esc(l.redactedSample || '')))
|
|
7992
|
+
.join('');
|
|
7913
7993
|
return (
|
|
7914
|
-
'<div class="scan-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
' ' +
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
'<span class="scan-agent-stat">' +
|
|
7921
|
-
(src.sessions || 0) +
|
|
7922
|
-
' session' +
|
|
7923
|
-
((src.sessions || 0) !== 1 ? 's' : '') +
|
|
7994
|
+
'<div class="scan-rule-row" onclick="var d=document.getElementById(\\'' +
|
|
7995
|
+
detailId +
|
|
7996
|
+
"');var a=this.querySelector('.scan-rule-arrow');d.style.display=d.style.display===''?'none':'';a.textContent=d.style.display===''?'\u25B2':'\u25BC';\\">" +
|
|
7997
|
+
'<span class="scan-verdict-badge badge-dlp">DLP</span>' +
|
|
7998
|
+
'<span class="scan-rule-name">' +
|
|
7999
|
+
esc(pattern) +
|
|
7924
8000
|
'</span>' +
|
|
7925
|
-
'<span class="scan-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
'
|
|
7929
|
-
|
|
8001
|
+
'<span class="scan-rule-bar-wrap"><span class="scan-rule-bar" style="width:' +
|
|
8002
|
+
barPct +
|
|
8003
|
+
'%"></span></span>' +
|
|
8004
|
+
'<span class="scan-rule-count">' +
|
|
8005
|
+
count +
|
|
7930
8006
|
'</span>' +
|
|
8007
|
+
'<span class="scan-rule-arrow">\u25BC</span>' +
|
|
8008
|
+
'</div><div id="' +
|
|
8009
|
+
detailId +
|
|
8010
|
+
'" style="display:none" class="scan-rule-detail">' +
|
|
8011
|
+
rows +
|
|
7931
8012
|
'</div>'
|
|
7932
8013
|
);
|
|
7933
|
-
})
|
|
7934
|
-
|
|
7935
|
-
return '<div class="scan-agent-group">' + rows.join('') + '</div>';
|
|
7936
|
-
})();
|
|
7937
|
-
|
|
7938
|
-
let html = stats + agentBreakdown;
|
|
7939
|
-
|
|
7940
|
-
if (allLeaks.length) {
|
|
7941
|
-
html +=
|
|
7942
|
-
'<div class="scan-rule-section-label" style="color:#e5534b">\u{1F511} Credential Leaks \u2014 secrets found in tool calls</div>';
|
|
7943
|
-
html += ruleSection(allLeaks, 'leak', 'badge-dlp', 'DLP', maxBar);
|
|
7944
|
-
}
|
|
7945
|
-
|
|
7946
|
-
if (blocked.length) {
|
|
7947
|
-
html +=
|
|
7948
|
-
'<div class="scan-rule-section-label" style="color:#e5534b">\u{1F6D1} Blocked \u2014 Node9 would have stopped these</div>';
|
|
7949
|
-
html += ruleSection(blocked, 'risk', 'badge-block', 'BLOCK', maxBar);
|
|
7950
|
-
}
|
|
7951
|
-
|
|
7952
|
-
if (supervised.length) {
|
|
7953
|
-
html +=
|
|
7954
|
-
'<div class="scan-rule-section-label">\u{1F441} Supervised \u2014 Node9 would have asked you first</div>';
|
|
7955
|
-
html += ruleSection(supervised, 'risk', 'badge-review', 'REVIEW', maxBar);
|
|
8014
|
+
})
|
|
8015
|
+
.join('');
|
|
7956
8016
|
}
|
|
7957
8017
|
|
|
7958
|
-
|
|
8018
|
+
// \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
|
|
8019
|
+
for (const section of sections) {
|
|
7959
8020
|
html +=
|
|
7960
|
-
'<div class="scan-rule-section-label" style="color
|
|
7961
|
-
|
|
8021
|
+
'<div class="scan-rule-section-label" style="color:' +
|
|
8022
|
+
sectionColor(section) +
|
|
8023
|
+
'">' +
|
|
8024
|
+
sectionHeading(section) +
|
|
8025
|
+
'</div>';
|
|
8026
|
+
html += ruleCardsForSection(section);
|
|
7962
8027
|
}
|
|
7963
8028
|
|
|
7964
|
-
// Loops: single collapsed summary card, expand to see all groups
|
|
7965
|
-
if (
|
|
7966
|
-
const totalReps =
|
|
8029
|
+
// \u2500\u2500 Loops: single collapsed summary card, expand to see all groups \u2500\u2500\u2500
|
|
8030
|
+
if (loops.length) {
|
|
8031
|
+
const totalReps = loops.reduce((sum, l) => sum + (l.count || 0), 0);
|
|
7967
8032
|
const loopsDetailId = 'loops-detail-' + Math.random().toString(36).slice(2);
|
|
7968
8033
|
const savingNote = savingsStr
|
|
7969
8034
|
? ' \xB7 <span style="color:var(--warning)">~' + savingsStr + ' wasted</span>'
|
|
7970
8035
|
: '';
|
|
7971
|
-
const
|
|
8036
|
+
const maxLoopCount = Math.max(...loops.map((l) => l.count || 0), 1);
|
|
8037
|
+
const loopGroupRows = loops
|
|
7972
8038
|
.map((item) => {
|
|
7973
8039
|
const date = new Date(item.timestamp).toLocaleDateString([], {
|
|
7974
8040
|
month: 'short',
|
|
7975
8041
|
day: 'numeric',
|
|
7976
8042
|
});
|
|
7977
|
-
const barPct = Math.round(
|
|
7978
|
-
(item.count / Math.max(...allLoops.map((l) => l.count), 1)) * 100
|
|
7979
|
-
);
|
|
8043
|
+
const barPct = Math.round((item.count / maxLoopCount) * 100);
|
|
7980
8044
|
const detailId = 'detail-' + Math.random().toString(36).slice(2);
|
|
7981
8045
|
return (
|
|
7982
8046
|
'<div class="scan-rule-row" onclick="event.stopPropagation();var d=document.getElementById(\\'' +
|
|
@@ -8014,9 +8078,9 @@ var init_ui = __esm({
|
|
|
8014
8078
|
"');var a=this.querySelector('.scan-rule-arrow');d.style.display=d.style.display===''?'none':'';a.textContent=d.style.display===''?'\u25B2':'\u25BC';\\">" +
|
|
8015
8079
|
'<span class="scan-verdict-badge badge-review">LOOP</span>' +
|
|
8016
8080
|
'<span class="scan-rule-name">' +
|
|
8017
|
-
|
|
8081
|
+
loops.length +
|
|
8018
8082
|
' pattern' +
|
|
8019
|
-
(
|
|
8083
|
+
(loops.length !== 1 ? 's' : '') +
|
|
8020
8084
|
' \xB7 ' +
|
|
8021
8085
|
totalReps +
|
|
8022
8086
|
' total repetitions' +
|
|
@@ -8090,10 +8154,20 @@ var init_ui = __esm({
|
|
|
8090
8154
|
const data = await res.json();
|
|
8091
8155
|
results.style.display = 'block';
|
|
8092
8156
|
|
|
8093
|
-
if (data.status === 'complete') {
|
|
8094
|
-
const
|
|
8095
|
-
const
|
|
8096
|
-
const
|
|
8157
|
+
if (data.status === 'complete' && data.summary) {
|
|
8158
|
+
const summary = data.summary;
|
|
8159
|
+
const byVerdict = summary.byVerdict || {};
|
|
8160
|
+
const sections = summary.sections || [];
|
|
8161
|
+
const leaks = summary.leaks || [];
|
|
8162
|
+
// Flatten rule findings across all sections so we can show a Top-N list
|
|
8163
|
+
const allRisks = [];
|
|
8164
|
+
for (const section of sections) {
|
|
8165
|
+
for (const rule of section.rules || []) {
|
|
8166
|
+
for (const f of rule.findings) {
|
|
8167
|
+
allRisks.push({ rule: rule.name, verdict: rule.verdict, ...f });
|
|
8168
|
+
}
|
|
8169
|
+
}
|
|
8170
|
+
}
|
|
8097
8171
|
|
|
8098
8172
|
let risksHtml = '';
|
|
8099
8173
|
if (allRisks.length > 0) {
|
|
@@ -8106,7 +8180,7 @@ var init_ui = __esm({
|
|
|
8106
8180
|
(r) => \`
|
|
8107
8181
|
<div class="finding-item">
|
|
8108
8182
|
<span class="finding-ts">\${new Date(r.timestamp).toLocaleDateString([], { month: 'short', day: 'numeric' })}</span>
|
|
8109
|
-
<span class="finding-rule">\${r.rule
|
|
8183
|
+
<span class="finding-rule">\${esc(r.rule)}</span>
|
|
8110
8184
|
<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>
|
|
8111
8185
|
</div>
|
|
8112
8186
|
\`
|
|
@@ -8117,18 +8191,18 @@ var init_ui = __esm({
|
|
|
8117
8191
|
}
|
|
8118
8192
|
|
|
8119
8193
|
let leaksHtml = '';
|
|
8120
|
-
if (
|
|
8194
|
+
if (leaks.length > 0) {
|
|
8121
8195
|
leaksHtml = \`
|
|
8122
8196
|
<div class="findings-list" style="border-color: rgba(201, 60, 55, 0.3);">
|
|
8123
8197
|
<div style="font-size: 10px; font-weight: 700; color: #f87171; margin-bottom: 8px; text-transform: uppercase;">Credential Leaks Identified</div>
|
|
8124
|
-
\${
|
|
8198
|
+
\${leaks
|
|
8125
8199
|
.slice(0, 5)
|
|
8126
8200
|
.map(
|
|
8127
8201
|
(l) => \`
|
|
8128
8202
|
<div class="finding-item">
|
|
8129
8203
|
<span class="finding-ts">\${new Date(l.timestamp).toLocaleDateString([], { month: 'short', day: 'numeric' })}</span>
|
|
8130
|
-
<span class="finding-rule">\${esc(l.
|
|
8131
|
-
<span class="finding-cmd"><span class="finding-badge badge-leak">\u{1F511}</span>\${esc(l.
|
|
8204
|
+
<span class="finding-rule">\${esc(l.patternName)}</span>
|
|
8205
|
+
<span class="finding-cmd"><span class="finding-badge badge-leak">\u{1F511}</span>\${esc(l.redactedSample)}</span>
|
|
8132
8206
|
</div>
|
|
8133
8207
|
\`
|
|
8134
8208
|
)
|
|
@@ -8137,10 +8211,11 @@ var init_ui = __esm({
|
|
|
8137
8211
|
\`;
|
|
8138
8212
|
}
|
|
8139
8213
|
|
|
8214
|
+
const riskCount = (byVerdict.blocked || 0) + (byVerdict.supervised || 0);
|
|
8140
8215
|
results.innerHTML = \`
|
|
8141
8216
|
<div class="insight-hint" style="color: #57ab5a; border-color: rgba(87, 171, 90, 0.4); background: rgba(87, 171, 90, 0.08);">
|
|
8142
8217
|
<strong>\u2705 History Audit Complete</strong><br>
|
|
8143
|
-
Scanned \${
|
|
8218
|
+
Scanned \${summary.stats?.sessions || 0} sessions. Found <strong>\${riskCount} risky operations</strong> and <strong>\${byVerdict.leaks || 0} secret leaks</strong>.
|
|
8144
8219
|
Node9 is now actively protecting you from these types of events.
|
|
8145
8220
|
</div>
|
|
8146
8221
|
\${leaksHtml}
|
|
@@ -8225,6 +8300,195 @@ var init_daemon_starter = __esm({
|
|
|
8225
8300
|
}
|
|
8226
8301
|
});
|
|
8227
8302
|
|
|
8303
|
+
// src/scan-summary.ts
|
|
8304
|
+
function buildScanSummary(agents) {
|
|
8305
|
+
const stats = {
|
|
8306
|
+
sessions: 0,
|
|
8307
|
+
totalToolCalls: 0,
|
|
8308
|
+
bashCalls: 0,
|
|
8309
|
+
totalCostUSD: 0,
|
|
8310
|
+
firstDate: null,
|
|
8311
|
+
lastDate: null
|
|
8312
|
+
};
|
|
8313
|
+
for (const a of agents) {
|
|
8314
|
+
stats.sessions += a.scan.sessions;
|
|
8315
|
+
stats.totalToolCalls += a.scan.totalToolCalls;
|
|
8316
|
+
stats.bashCalls += a.scan.bashCalls;
|
|
8317
|
+
stats.totalCostUSD += a.scan.totalCostUSD;
|
|
8318
|
+
if (a.scan.firstDate && (!stats.firstDate || a.scan.firstDate < stats.firstDate)) {
|
|
8319
|
+
stats.firstDate = a.scan.firstDate;
|
|
8320
|
+
}
|
|
8321
|
+
if (a.scan.lastDate && (!stats.lastDate || a.scan.lastDate > stats.lastDate)) {
|
|
8322
|
+
stats.lastDate = a.scan.lastDate;
|
|
8323
|
+
}
|
|
8324
|
+
}
|
|
8325
|
+
const allFindings = agents.flatMap((a) => a.scan.findings);
|
|
8326
|
+
const allLeaks = agents.flatMap(
|
|
8327
|
+
(a) => a.scan.dlpFindings.map((f) => ({
|
|
8328
|
+
patternName: f.patternName,
|
|
8329
|
+
redactedSample: f.redactedSample,
|
|
8330
|
+
toolName: f.toolName,
|
|
8331
|
+
timestamp: f.timestamp,
|
|
8332
|
+
project: f.project,
|
|
8333
|
+
sessionId: f.sessionId,
|
|
8334
|
+
agent: f.agent
|
|
8335
|
+
}))
|
|
8336
|
+
);
|
|
8337
|
+
const allLoops = agents.flatMap(
|
|
8338
|
+
(a) => a.scan.loopFindings.map((f) => ({
|
|
8339
|
+
toolName: f.toolName,
|
|
8340
|
+
commandPreview: f.commandPreview,
|
|
8341
|
+
count: f.count,
|
|
8342
|
+
timestamp: f.timestamp,
|
|
8343
|
+
project: f.project,
|
|
8344
|
+
sessionId: f.sessionId,
|
|
8345
|
+
agent: f.agent
|
|
8346
|
+
}))
|
|
8347
|
+
);
|
|
8348
|
+
const byVerdict = {
|
|
8349
|
+
blocked: allFindings.filter((f) => f.source.rule.verdict === "block").length,
|
|
8350
|
+
supervised: allFindings.filter((f) => f.source.rule.verdict === "review").length,
|
|
8351
|
+
leaks: allLeaks.length,
|
|
8352
|
+
loops: allLoops.length
|
|
8353
|
+
};
|
|
8354
|
+
const byAgent = agents.map((a) => ({
|
|
8355
|
+
id: a.id,
|
|
8356
|
+
label: a.label,
|
|
8357
|
+
icon: a.icon,
|
|
8358
|
+
sessions: a.scan.sessions,
|
|
8359
|
+
findings: a.scan.findings.length + a.scan.dlpFindings.length + a.scan.loopFindings.length,
|
|
8360
|
+
costUSD: a.scan.totalCostUSD
|
|
8361
|
+
})).filter((s) => s.sessions > 0 || s.findings > 0);
|
|
8362
|
+
const sections = buildSections(allFindings);
|
|
8363
|
+
const wastedIters = allLoops.reduce(
|
|
8364
|
+
(sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE),
|
|
8365
|
+
0
|
|
8366
|
+
);
|
|
8367
|
+
const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
|
|
8368
|
+
return {
|
|
8369
|
+
stats,
|
|
8370
|
+
byVerdict,
|
|
8371
|
+
byAgent,
|
|
8372
|
+
sections,
|
|
8373
|
+
leaks: allLeaks,
|
|
8374
|
+
loops: allLoops,
|
|
8375
|
+
loopWastedUSD
|
|
8376
|
+
};
|
|
8377
|
+
}
|
|
8378
|
+
function buildSections(findings) {
|
|
8379
|
+
const sectionMap = /* @__PURE__ */ new Map();
|
|
8380
|
+
function ensureSection(id, label, subtitle, sourceType, shieldKey) {
|
|
8381
|
+
let s = sectionMap.get(id);
|
|
8382
|
+
if (!s) {
|
|
8383
|
+
s = {
|
|
8384
|
+
id,
|
|
8385
|
+
label,
|
|
8386
|
+
subtitle,
|
|
8387
|
+
sourceType,
|
|
8388
|
+
shieldKey,
|
|
8389
|
+
blockedCount: 0,
|
|
8390
|
+
reviewCount: 0,
|
|
8391
|
+
rules: []
|
|
8392
|
+
};
|
|
8393
|
+
sectionMap.set(id, s);
|
|
8394
|
+
}
|
|
8395
|
+
return s;
|
|
8396
|
+
}
|
|
8397
|
+
const ruleMap = /* @__PURE__ */ new Map();
|
|
8398
|
+
for (const f of findings) {
|
|
8399
|
+
const src = f.source;
|
|
8400
|
+
const sourceType = src.sourceType;
|
|
8401
|
+
const shieldName = src.shieldName;
|
|
8402
|
+
const verdict = src.rule.verdict === "block" ? "block" : "review";
|
|
8403
|
+
let sectionId;
|
|
8404
|
+
let sectionLabel;
|
|
8405
|
+
let sectionSubtitle;
|
|
8406
|
+
let shieldKey;
|
|
8407
|
+
if (sourceType === "default") {
|
|
8408
|
+
sectionId = "default";
|
|
8409
|
+
sectionLabel = "Default Rules";
|
|
8410
|
+
sectionSubtitle = "built-in, always on";
|
|
8411
|
+
} else if (sourceType === "shield") {
|
|
8412
|
+
sectionId = `shield:${shieldName}`;
|
|
8413
|
+
sectionLabel = shieldName;
|
|
8414
|
+
sectionSubtitle = SHIELDS[shieldName]?.description ?? "";
|
|
8415
|
+
shieldKey = shieldName;
|
|
8416
|
+
} else if (shieldName === "cloud") {
|
|
8417
|
+
sectionId = "cloud";
|
|
8418
|
+
sectionLabel = "Cloud Policy";
|
|
8419
|
+
sectionSubtitle = "synced from node9 cloud";
|
|
8420
|
+
} else {
|
|
8421
|
+
sectionId = "user";
|
|
8422
|
+
sectionLabel = "Your Rules";
|
|
8423
|
+
sectionSubtitle = "added in node9.config.json";
|
|
8424
|
+
}
|
|
8425
|
+
const section = ensureSection(sectionId, sectionLabel, sectionSubtitle, sourceType, shieldKey);
|
|
8426
|
+
const ruleDisplayName = (src.rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
|
|
8427
|
+
const ruleKey = sectionId + "::" + ruleDisplayName;
|
|
8428
|
+
let rule = ruleMap.get(ruleKey);
|
|
8429
|
+
if (!rule) {
|
|
8430
|
+
rule = {
|
|
8431
|
+
name: ruleDisplayName,
|
|
8432
|
+
verdict,
|
|
8433
|
+
reason: src.rule.reason ?? "",
|
|
8434
|
+
findings: []
|
|
8435
|
+
};
|
|
8436
|
+
ruleMap.set(ruleKey, rule);
|
|
8437
|
+
section.rules.push(rule);
|
|
8438
|
+
}
|
|
8439
|
+
const cmdPreview = previewCommand(f.input, 120);
|
|
8440
|
+
const fullCmd = fullCommandOf(f.input);
|
|
8441
|
+
const isDupe = rule.findings.some((x) => x.project === f.project && x.command === cmdPreview);
|
|
8442
|
+
if (!isDupe) {
|
|
8443
|
+
rule.findings.push({
|
|
8444
|
+
timestamp: f.timestamp ?? "",
|
|
8445
|
+
command: cmdPreview,
|
|
8446
|
+
fullCommand: fullCmd,
|
|
8447
|
+
project: f.project,
|
|
8448
|
+
sessionId: f.sessionId,
|
|
8449
|
+
agent: f.agent,
|
|
8450
|
+
toolName: f.toolName
|
|
8451
|
+
});
|
|
8452
|
+
}
|
|
8453
|
+
if (verdict === "block") section.blockedCount++;
|
|
8454
|
+
else section.reviewCount++;
|
|
8455
|
+
}
|
|
8456
|
+
const sections = [...sectionMap.values()];
|
|
8457
|
+
sections.sort((a, b) => {
|
|
8458
|
+
const aTotal = a.blockedCount + a.reviewCount;
|
|
8459
|
+
const bTotal = b.blockedCount + b.reviewCount;
|
|
8460
|
+
if (b.blockedCount !== a.blockedCount) return b.blockedCount - a.blockedCount;
|
|
8461
|
+
return bTotal - aTotal;
|
|
8462
|
+
});
|
|
8463
|
+
for (const s of sections) {
|
|
8464
|
+
s.rules.sort((a, b) => {
|
|
8465
|
+
const aBlock = a.verdict === "block" ? 1 : 0;
|
|
8466
|
+
const bBlock = b.verdict === "block" ? 1 : 0;
|
|
8467
|
+
if (bBlock !== aBlock) return bBlock - aBlock;
|
|
8468
|
+
return b.findings.length - a.findings.length;
|
|
8469
|
+
});
|
|
8470
|
+
}
|
|
8471
|
+
return sections;
|
|
8472
|
+
}
|
|
8473
|
+
function previewCommand(input, max) {
|
|
8474
|
+
const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
|
|
8475
|
+
const s = String(raw).replace(/\s+/g, " ").trim();
|
|
8476
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
8477
|
+
}
|
|
8478
|
+
function fullCommandOf(input) {
|
|
8479
|
+
const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
|
|
8480
|
+
return String(raw).replace(/\s+/g, " ").trim();
|
|
8481
|
+
}
|
|
8482
|
+
var LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD;
|
|
8483
|
+
var init_scan_summary = __esm({
|
|
8484
|
+
"src/scan-summary.ts"() {
|
|
8485
|
+
"use strict";
|
|
8486
|
+
init_shields();
|
|
8487
|
+
LOOP_THRESHOLD_FOR_WASTE = 3;
|
|
8488
|
+
COST_PER_LOOP_ITER_USD = 6e-3;
|
|
8489
|
+
}
|
|
8490
|
+
});
|
|
8491
|
+
|
|
8228
8492
|
// src/cli/commands/scan.ts
|
|
8229
8493
|
import chalk2 from "chalk";
|
|
8230
8494
|
import fs13 from "fs";
|
|
@@ -8269,10 +8533,6 @@ function preview(input, max) {
|
|
|
8269
8533
|
const s = String(cmd).replace(/\s+/g, " ").trim();
|
|
8270
8534
|
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
8271
8535
|
}
|
|
8272
|
-
function fullCommand(input) {
|
|
8273
|
-
const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
|
|
8274
|
-
return String(cmd).replace(/\s+/g, " ").trim();
|
|
8275
|
-
}
|
|
8276
8536
|
function detectLoops(calls, project, sessionId, agent) {
|
|
8277
8537
|
const counts = /* @__PURE__ */ new Map();
|
|
8278
8538
|
for (const call of calls) {
|
|
@@ -9043,26 +9303,32 @@ function printFindingRow(f, drillDown, showSessionId, previewWidth) {
|
|
|
9043
9303
|
const ts = f.timestamp ? chalk2.dim(fmtTs(f.timestamp) + " ") : "";
|
|
9044
9304
|
const proj = chalk2.dim(f.project.slice(0, 22).padEnd(22) + " ");
|
|
9045
9305
|
const agentBadge = f.agent === "gemini" ? chalk2.blue("[Gemini] ") : f.agent === "codex" ? chalk2.magenta("[Codex] ") : chalk2.cyan("[Claude] ");
|
|
9046
|
-
|
|
9306
|
+
let cmdText;
|
|
9307
|
+
if (drillDown) {
|
|
9308
|
+
cmdText = f.fullCommand;
|
|
9309
|
+
} else {
|
|
9310
|
+
cmdText = f.command;
|
|
9311
|
+
if (cmdText.length > previewWidth) cmdText = cmdText.slice(0, previewWidth - 1) + "\u2026";
|
|
9312
|
+
}
|
|
9313
|
+
const cmd = chalk2.gray(cmdText);
|
|
9047
9314
|
const sessionSuffix = showSessionId && f.sessionId ? chalk2.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
|
|
9048
9315
|
console.log(` ${ts}${proj}${agentBadge}${cmd}${sessionSuffix}`);
|
|
9049
9316
|
}
|
|
9050
|
-
function printRuleGroup(
|
|
9051
|
-
const
|
|
9052
|
-
const ruleCount =
|
|
9317
|
+
function printRuleGroup(rule, topN, drillDown, previewWidth) {
|
|
9318
|
+
const findings = rule.findings;
|
|
9319
|
+
const ruleCount = findings.length;
|
|
9053
9320
|
const countBadge = ruleCount > 1 ? chalk2.white(` \xD7${ruleCount}`) : "";
|
|
9054
|
-
const
|
|
9055
|
-
const icon = verdictIcon(rule.verdict ?? "review");
|
|
9321
|
+
const icon = verdictIcon(rule.verdict);
|
|
9056
9322
|
console.log(
|
|
9057
|
-
" " + icon + " " + chalk2.white(
|
|
9323
|
+
" " + icon + " " + chalk2.white(rule.name) + countBadge + (rule.reason ? chalk2.dim(` \u2014 ${rule.reason}`) : "")
|
|
9058
9324
|
);
|
|
9059
|
-
const shown = drillDown ?
|
|
9325
|
+
const shown = drillDown ? findings : findings.slice(0, topN);
|
|
9060
9326
|
for (const f of shown) {
|
|
9061
9327
|
printFindingRow(f, drillDown, drillDown, previewWidth);
|
|
9062
9328
|
}
|
|
9063
|
-
if (!drillDown &&
|
|
9329
|
+
if (!drillDown && findings.length > topN) {
|
|
9064
9330
|
console.log(
|
|
9065
|
-
chalk2.dim(` \u2026 and ${
|
|
9331
|
+
chalk2.dim(` \u2026 and ${findings.length - topN} more (--drill-down for full list)`)
|
|
9066
9332
|
);
|
|
9067
9333
|
}
|
|
9068
9334
|
}
|
|
@@ -9109,6 +9375,11 @@ function registerScanCommand(program2) {
|
|
|
9109
9375
|
(done) => onProgress(claudeScan.filesScanned + geminiScan.filesScanned + done)
|
|
9110
9376
|
);
|
|
9111
9377
|
const scan = mergeScans(mergeScans(claudeScan, geminiScan), codexScan);
|
|
9378
|
+
const summary = buildScanSummary([
|
|
9379
|
+
{ id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
|
|
9380
|
+
{ id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
|
|
9381
|
+
{ id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
|
|
9382
|
+
]);
|
|
9112
9383
|
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
9113
9384
|
if (scan.filesScanned === 0) {
|
|
9114
9385
|
console.log(chalk2.yellow(" No session history found."));
|
|
@@ -9171,76 +9442,27 @@ function registerScanCommand(program2) {
|
|
|
9171
9442
|
);
|
|
9172
9443
|
}
|
|
9173
9444
|
console.log("");
|
|
9174
|
-
const sections
|
|
9175
|
-
const defaultFindings = scan.findings.filter((f) => f.source.sourceType === "default");
|
|
9176
|
-
if (defaultFindings.length > 0) {
|
|
9177
|
-
sections.push({
|
|
9178
|
-
label: "Default Rules",
|
|
9179
|
-
subtitle: "built-in, always on",
|
|
9180
|
-
findings: defaultFindings
|
|
9181
|
-
});
|
|
9182
|
-
}
|
|
9183
|
-
const byShield = /* @__PURE__ */ new Map();
|
|
9184
|
-
for (const f of scan.findings.filter((f2) => f2.source.sourceType === "shield")) {
|
|
9185
|
-
const arr = byShield.get(f.source.shieldName) ?? [];
|
|
9186
|
-
arr.push(f);
|
|
9187
|
-
byShield.set(f.source.shieldName, arr);
|
|
9188
|
-
}
|
|
9189
|
-
const shieldsWithFindings = [...byShield.entries()].sort(
|
|
9190
|
-
(a, b) => b[1].length - a[1].length
|
|
9191
|
-
);
|
|
9192
|
-
for (const [shieldName, findings] of shieldsWithFindings) {
|
|
9193
|
-
const description = SHIELDS[shieldName]?.description ?? "";
|
|
9194
|
-
sections.push({
|
|
9195
|
-
label: shieldName,
|
|
9196
|
-
subtitle: description,
|
|
9197
|
-
shieldKey: shieldName,
|
|
9198
|
-
findings
|
|
9199
|
-
});
|
|
9200
|
-
}
|
|
9201
|
-
const userFindings = scan.findings.filter(
|
|
9202
|
-
(f) => f.source.sourceType === "user" || f.source.shieldName === "cloud"
|
|
9203
|
-
);
|
|
9204
|
-
if (userFindings.length > 0) {
|
|
9205
|
-
sections.push({
|
|
9206
|
-
label: "Your Rules",
|
|
9207
|
-
subtitle: "added in node9.config.json",
|
|
9208
|
-
findings: userFindings
|
|
9209
|
-
});
|
|
9210
|
-
}
|
|
9211
|
-
for (const section of sections) {
|
|
9212
|
-
const sectionBlocked = section.findings.filter(
|
|
9213
|
-
(f) => f.source.rule.verdict === "block"
|
|
9214
|
-
).length;
|
|
9215
|
-
const sectionReview = section.findings.length - sectionBlocked;
|
|
9445
|
+
for (const section of summary.sections) {
|
|
9216
9446
|
const countParts = [];
|
|
9217
|
-
if (
|
|
9218
|
-
|
|
9447
|
+
if (section.blockedCount > 0)
|
|
9448
|
+
countParts.push(chalk2.red(`${section.blockedCount} blocked`));
|
|
9449
|
+
if (section.reviewCount > 0)
|
|
9450
|
+
countParts.push(chalk2.yellow(`${section.reviewCount} review`));
|
|
9219
9451
|
const countStr = countParts.join(chalk2.dim(" \xB7 "));
|
|
9220
9452
|
const enableHint = section.shieldKey ? chalk2.dim(` \u2192 node9 shield enable ${section.shieldKey}`) : "";
|
|
9221
9453
|
console.log(" " + chalk2.dim("\u2500".repeat(70)));
|
|
9222
9454
|
console.log(
|
|
9223
9455
|
" " + chalk2.bold(section.label) + (section.subtitle ? chalk2.dim(` \xB7 ${section.subtitle}`) : "") + " " + countStr + enableHint
|
|
9224
9456
|
);
|
|
9225
|
-
const
|
|
9226
|
-
|
|
9227
|
-
const ruleKey = f.source.rule.name ?? "unnamed";
|
|
9228
|
-
const arr = byRule.get(ruleKey) ?? [];
|
|
9229
|
-
arr.push(f);
|
|
9230
|
-
byRule.set(ruleKey, arr);
|
|
9231
|
-
}
|
|
9232
|
-
const sortedRules = [...byRule.entries()].sort((a, b) => {
|
|
9233
|
-
const aBlock = a[1][0].source.rule.verdict === "block" ? 1 : 0;
|
|
9234
|
-
const bBlock = b[1][0].source.rule.verdict === "block" ? 1 : 0;
|
|
9235
|
-
if (bBlock !== aBlock) return bBlock - aBlock;
|
|
9236
|
-
return b[1].length - a[1].length;
|
|
9237
|
-
});
|
|
9238
|
-
for (const [, ruleFindings] of sortedRules) {
|
|
9239
|
-
printRuleGroup(ruleFindings, topN, drillDown, previewWidth);
|
|
9457
|
+
for (const rule of section.rules) {
|
|
9458
|
+
printRuleGroup(rule, topN, drillDown, previewWidth);
|
|
9240
9459
|
}
|
|
9241
9460
|
console.log("");
|
|
9242
9461
|
}
|
|
9243
|
-
const
|
|
9462
|
+
const activeShieldIds = new Set(
|
|
9463
|
+
summary.sections.filter((s) => s.sourceType === "shield" && s.shieldKey).map((s) => s.shieldKey)
|
|
9464
|
+
);
|
|
9465
|
+
const emptyShields = Object.keys(SHIELDS).filter((n) => !activeShieldIds.has(n)).sort();
|
|
9244
9466
|
if (emptyShields.length > 0) {
|
|
9245
9467
|
console.log(" " + chalk2.dim("\u2500".repeat(70)));
|
|
9246
9468
|
console.log(
|
|
@@ -9350,78 +9572,12 @@ function registerScanCommand(program2) {
|
|
|
9350
9572
|
const internalToken = getInternalToken();
|
|
9351
9573
|
if (internalToken) {
|
|
9352
9574
|
try {
|
|
9353
|
-
const
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
verdict: f.source?.rule?.verdict ?? f.source?.rule?.action ?? "review",
|
|
9360
|
-
ruleSource: f.source?.sourceType ?? "default",
|
|
9361
|
-
source: src
|
|
9362
|
-
}))
|
|
9363
|
-
);
|
|
9364
|
-
const mapLeaks = (arr, src) => (
|
|
9365
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9366
|
-
arr.map((f) => ({
|
|
9367
|
-
timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
9368
|
-
pattern: f.patternName || "DLP",
|
|
9369
|
-
sample: f.redactedSample || "********",
|
|
9370
|
-
source: src
|
|
9371
|
-
}))
|
|
9372
|
-
);
|
|
9373
|
-
const mapLoops = (arr, src) => (
|
|
9374
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9375
|
-
arr.map((f) => ({
|
|
9376
|
-
timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
9377
|
-
toolName: f.toolName || "unknown",
|
|
9378
|
-
commandPreview: f.commandPreview || "",
|
|
9379
|
-
count: f.count || 0,
|
|
9380
|
-
source: src
|
|
9381
|
-
}))
|
|
9382
|
-
);
|
|
9383
|
-
const sources = [
|
|
9384
|
-
{
|
|
9385
|
-
id: "claude",
|
|
9386
|
-
label: "Claude",
|
|
9387
|
-
icon: "\u{1F916}",
|
|
9388
|
-
sessions: claudeScan.sessions,
|
|
9389
|
-
findings: mapFindings(claudeScan.findings, "claude"),
|
|
9390
|
-
leaks: mapLeaks(claudeScan.dlpFindings, "claude"),
|
|
9391
|
-
loops: mapLoops(claudeScan.loopFindings, "claude")
|
|
9392
|
-
},
|
|
9393
|
-
{
|
|
9394
|
-
id: "gemini",
|
|
9395
|
-
label: "Gemini",
|
|
9396
|
-
icon: "\u264A",
|
|
9397
|
-
sessions: geminiScan.sessions,
|
|
9398
|
-
findings: mapFindings(geminiScan.findings, "gemini"),
|
|
9399
|
-
leaks: mapLeaks(geminiScan.dlpFindings, "gemini"),
|
|
9400
|
-
loops: mapLoops(geminiScan.loopFindings, "gemini")
|
|
9401
|
-
},
|
|
9402
|
-
{
|
|
9403
|
-
id: "codex",
|
|
9404
|
-
label: "Codex",
|
|
9405
|
-
icon: "\u{1F52E}",
|
|
9406
|
-
sessions: codexScan.sessions,
|
|
9407
|
-
findings: mapFindings(codexScan.findings, "codex"),
|
|
9408
|
-
leaks: mapLeaks(codexScan.dlpFindings, "codex"),
|
|
9409
|
-
loops: mapLoops(codexScan.loopFindings, "codex")
|
|
9410
|
-
}
|
|
9411
|
-
].filter(
|
|
9412
|
-
(s) => s.sessions > 0 || s.findings.length > 0 || s.leaks.length > 0 || s.loops.length > 0
|
|
9413
|
-
);
|
|
9414
|
-
const payload = {
|
|
9415
|
-
status: "complete",
|
|
9416
|
-
summary: {
|
|
9417
|
-
sessions: scan.sessions,
|
|
9418
|
-
findings: scan.findings.length,
|
|
9419
|
-
dlp: scan.dlpFindings.length,
|
|
9420
|
-
loops: scan.loopFindings.length,
|
|
9421
|
-
totalCostUSD: scan.totalCostUSD
|
|
9422
|
-
},
|
|
9423
|
-
sources
|
|
9424
|
-
};
|
|
9575
|
+
const summary2 = buildScanSummary([
|
|
9576
|
+
{ id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
|
|
9577
|
+
{ id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
|
|
9578
|
+
{ id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
|
|
9579
|
+
]);
|
|
9580
|
+
const payload = { status: "complete", summary: summary2 };
|
|
9425
9581
|
await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/scan/push`, {
|
|
9426
9582
|
method: "POST",
|
|
9427
9583
|
headers: { "Content-Type": "application/json", "x-node9-internal": internalToken },
|
|
@@ -9448,6 +9604,7 @@ var init_scan = __esm({
|
|
|
9448
9604
|
init_dlp();
|
|
9449
9605
|
init_daemon();
|
|
9450
9606
|
init_daemon_starter();
|
|
9607
|
+
init_scan_summary();
|
|
9451
9608
|
CLAUDE_PRICING = {
|
|
9452
9609
|
"claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
9453
9610
|
"claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
@@ -11430,9 +11587,21 @@ data: ${JSON.stringify(item.data)}
|
|
|
11430
11587
|
const d = /* @__PURE__ */ new Date();
|
|
11431
11588
|
d.setDate(d.getDate() - 90);
|
|
11432
11589
|
d.setHours(0, 0, 0, 0);
|
|
11433
|
-
|
|
11434
|
-
|
|
11435
|
-
|
|
11590
|
+
const EMPTY_SCAN = {
|
|
11591
|
+
filesScanned: 0,
|
|
11592
|
+
sessions: 0,
|
|
11593
|
+
totalToolCalls: 0,
|
|
11594
|
+
bashCalls: 0,
|
|
11595
|
+
findings: [],
|
|
11596
|
+
dlpFindings: [],
|
|
11597
|
+
loopFindings: [],
|
|
11598
|
+
totalCostUSD: 0,
|
|
11599
|
+
firstDate: null,
|
|
11600
|
+
lastDate: null
|
|
11601
|
+
};
|
|
11602
|
+
let claude = EMPTY_SCAN;
|
|
11603
|
+
let gemini = EMPTY_SCAN;
|
|
11604
|
+
let codex = EMPTY_SCAN;
|
|
11436
11605
|
try {
|
|
11437
11606
|
claude = scanClaudeHistory(d);
|
|
11438
11607
|
} catch (e) {
|
|
@@ -11448,86 +11617,13 @@ data: ${JSON.stringify(item.data)}
|
|
|
11448
11617
|
} catch (e) {
|
|
11449
11618
|
console.error("Codex scan failed:", e);
|
|
11450
11619
|
}
|
|
11451
|
-
const
|
|
11452
|
-
|
|
11453
|
-
|
|
11454
|
-
|
|
11455
|
-
|
|
11456
|
-
command: f.input?.command ?? f.input?.cmd ?? f.input?.file_path ?? f.toolName ?? "unknown",
|
|
11457
|
-
verdict: f.source?.rule?.verdict ?? f.source?.rule?.action ?? "review",
|
|
11458
|
-
ruleSource: f.source?.sourceType ?? "default",
|
|
11459
|
-
source: src
|
|
11460
|
-
}))
|
|
11461
|
-
);
|
|
11462
|
-
const mapLeaks = (arr, src) => (
|
|
11463
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11464
|
-
arr.map((f) => ({
|
|
11465
|
-
timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
11466
|
-
pattern: f.patternName || "DLP",
|
|
11467
|
-
sample: f.redactedSample || "********",
|
|
11468
|
-
source: src
|
|
11469
|
-
}))
|
|
11470
|
-
);
|
|
11471
|
-
const mapLoops = (arr, src) => (
|
|
11472
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11473
|
-
arr.map((f) => ({
|
|
11474
|
-
timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
11475
|
-
toolName: f.toolName || "unknown",
|
|
11476
|
-
commandPreview: f.commandPreview || "",
|
|
11477
|
-
count: f.count || 0,
|
|
11478
|
-
source: src
|
|
11479
|
-
}))
|
|
11480
|
-
);
|
|
11481
|
-
const sources = [
|
|
11482
|
-
{
|
|
11483
|
-
id: "claude",
|
|
11484
|
-
label: "Claude",
|
|
11485
|
-
icon: "\u{1F916}",
|
|
11486
|
-
sessions: claude.sessions,
|
|
11487
|
-
findings: mapFindings(claude.findings, "claude"),
|
|
11488
|
-
leaks: mapLeaks(claude.dlpFindings, "claude"),
|
|
11489
|
-
loops: mapLoops(claude.loopFindings, "claude")
|
|
11490
|
-
},
|
|
11491
|
-
{
|
|
11492
|
-
id: "gemini",
|
|
11493
|
-
label: "Gemini",
|
|
11494
|
-
icon: "\u264A",
|
|
11495
|
-
sessions: gemini.sessions,
|
|
11496
|
-
findings: mapFindings(gemini.findings, "gemini"),
|
|
11497
|
-
leaks: mapLeaks(gemini.dlpFindings, "gemini"),
|
|
11498
|
-
loops: mapLoops(gemini.loopFindings, "gemini")
|
|
11499
|
-
},
|
|
11500
|
-
{
|
|
11501
|
-
id: "codex",
|
|
11502
|
-
label: "Codex",
|
|
11503
|
-
icon: "\u{1F52E}",
|
|
11504
|
-
sessions: codex.sessions,
|
|
11505
|
-
findings: mapFindings(codex.findings, "codex"),
|
|
11506
|
-
leaks: mapLeaks(codex.dlpFindings, "codex"),
|
|
11507
|
-
loops: mapLoops(codex.loopFindings, "codex")
|
|
11508
|
-
}
|
|
11509
|
-
].filter(
|
|
11510
|
-
(s) => s.sessions > 0 || s.findings.length > 0 || s.leaks.length > 0 || s.loops.length > 0
|
|
11511
|
-
);
|
|
11512
|
-
const totalSessions = claude.sessions + gemini.sessions + codex.sessions;
|
|
11513
|
-
const totalFindings = claude.findings.length + gemini.findings.length + codex.findings.length;
|
|
11514
|
-
const totalDlp = claude.dlpFindings.length + gemini.dlpFindings.length + codex.dlpFindings.length;
|
|
11515
|
-
const totalLoops = claude.loopFindings.length + gemini.loopFindings.length + codex.loopFindings.length;
|
|
11516
|
-
const totalCostUSD = (claude.totalCostUSD || 0) + (gemini.totalCostUSD || 0) + (codex.totalCostUSD || 0);
|
|
11620
|
+
const summary = buildScanSummary([
|
|
11621
|
+
{ id: "claude", label: "Claude", icon: "\u{1F916}", scan: claude },
|
|
11622
|
+
{ id: "gemini", label: "Gemini", icon: "\u264A", scan: gemini },
|
|
11623
|
+
{ id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codex }
|
|
11624
|
+
]);
|
|
11517
11625
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
11518
|
-
return res.end(
|
|
11519
|
-
JSON.stringify({
|
|
11520
|
-
status: "complete",
|
|
11521
|
-
summary: {
|
|
11522
|
-
sessions: totalSessions,
|
|
11523
|
-
totalCostUSD,
|
|
11524
|
-
findings: totalFindings,
|
|
11525
|
-
dlp: totalDlp,
|
|
11526
|
-
loops: totalLoops
|
|
11527
|
-
},
|
|
11528
|
-
sources
|
|
11529
|
-
})
|
|
11530
|
-
);
|
|
11626
|
+
return res.end(JSON.stringify({ status: "complete", summary }));
|
|
11531
11627
|
} catch (err2) {
|
|
11532
11628
|
console.error("Scan failed:", err2);
|
|
11533
11629
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -11866,6 +11962,7 @@ var init_server = __esm({
|
|
|
11866
11962
|
init_shields();
|
|
11867
11963
|
init_ui2();
|
|
11868
11964
|
init_scan();
|
|
11965
|
+
init_scan_summary();
|
|
11869
11966
|
init_state2();
|
|
11870
11967
|
init_state();
|
|
11871
11968
|
init_patch();
|
|
@@ -20520,10 +20617,10 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
20520
20617
|
program.help();
|
|
20521
20618
|
return;
|
|
20522
20619
|
}
|
|
20523
|
-
const
|
|
20620
|
+
const fullCommand = runArgs.join(" ");
|
|
20524
20621
|
let result = await authorizeHeadless(
|
|
20525
20622
|
"shell",
|
|
20526
|
-
{ command:
|
|
20623
|
+
{ command: fullCommand },
|
|
20527
20624
|
{
|
|
20528
20625
|
agent: "Terminal"
|
|
20529
20626
|
}
|
|
@@ -20531,11 +20628,11 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
20531
20628
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
20532
20629
|
console.error(chalk26.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
20533
20630
|
const daemonReady = await autoStartDaemonAndWait();
|
|
20534
|
-
if (daemonReady) result = await authorizeHeadless("shell", { command:
|
|
20631
|
+
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
20535
20632
|
}
|
|
20536
20633
|
if (result.noApprovalMechanism && process.stdout.isTTY) {
|
|
20537
20634
|
const approved = await confirm2({
|
|
20538
|
-
message: `\u{1F6E1}\uFE0F Node9: Allow "${
|
|
20635
|
+
message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand}"?`,
|
|
20539
20636
|
default: false
|
|
20540
20637
|
});
|
|
20541
20638
|
result = { approved, reason: approved ? void 0 : "Denied by user at terminal." };
|
|
@@ -20548,7 +20645,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
20548
20645
|
process.exit(1);
|
|
20549
20646
|
}
|
|
20550
20647
|
console.error(chalk26.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
20551
|
-
await runProxy(
|
|
20648
|
+
await runProxy(fullCommand);
|
|
20552
20649
|
} else {
|
|
20553
20650
|
program.help();
|
|
20554
20651
|
}
|