@node9/proxy 1.15.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -39
- package/dist/cli.js +771 -411
- package/dist/cli.mjs +726 -366
- package/dist/index.js +8 -4
- package/dist/index.mjs +8 -4
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -4709,15 +4709,17 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
4709
4709
|
if (!options?.calledFromDaemon) {
|
|
4710
4710
|
const actId = randomUUID();
|
|
4711
4711
|
const actTs = Date.now();
|
|
4712
|
+
const stripAnsi2 = (s) => s.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "");
|
|
4713
|
+
const sanitizedAgent = meta?.agent ? stripAnsi2(meta.agent).slice(0, 80) : void 0;
|
|
4714
|
+
const sanitizedMcpServer = meta?.mcpServer ? stripAnsi2(meta.mcpServer).slice(0, 40) : void 0;
|
|
4712
4715
|
const socketOk = await notifyActivity({
|
|
4713
4716
|
id: actId,
|
|
4714
4717
|
ts: actTs,
|
|
4715
4718
|
tool: toolName,
|
|
4716
4719
|
args,
|
|
4717
4720
|
status: "pending",
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
agent: meta?.agent ? meta.agent.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80) : void 0
|
|
4721
|
+
agent: sanitizedAgent,
|
|
4722
|
+
mcpServer: sanitizedMcpServer
|
|
4721
4723
|
});
|
|
4722
4724
|
const result = await _authorizeHeadlessCore(toolName, args, meta, {
|
|
4723
4725
|
...options,
|
|
@@ -4735,7 +4737,9 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
4735
4737
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
4736
4738
|
label: result.blockedByLabel,
|
|
4737
4739
|
ruleHit: result.ruleHit,
|
|
4738
|
-
observeWouldBlock: result.observeWouldBlock
|
|
4740
|
+
observeWouldBlock: result.observeWouldBlock,
|
|
4741
|
+
agent: sanitizedAgent,
|
|
4742
|
+
mcpServer: sanitizedMcpServer
|
|
4739
4743
|
});
|
|
4740
4744
|
}
|
|
4741
4745
|
return result;
|
|
@@ -10359,7 +10363,7 @@ function openBrowserLocal() {
|
|
|
10359
10363
|
} catch {
|
|
10360
10364
|
}
|
|
10361
10365
|
}
|
|
10362
|
-
async function autoStartDaemonAndWait(
|
|
10366
|
+
async function autoStartDaemonAndWait(openBrowser = false) {
|
|
10363
10367
|
if (isTestingMode()) return false;
|
|
10364
10368
|
if (!path15.isAbsolute(process.argv[1])) return false;
|
|
10365
10369
|
let resolvedArgv1;
|
|
@@ -10379,7 +10383,7 @@ async function autoStartDaemonAndWait(openBrowser2 = true) {
|
|
|
10379
10383
|
env: {
|
|
10380
10384
|
...process.env,
|
|
10381
10385
|
NODE9_AUTO_STARTED: "1",
|
|
10382
|
-
...
|
|
10386
|
+
...openBrowser && { NODE9_BROWSER_OPENED: "1" }
|
|
10383
10387
|
}
|
|
10384
10388
|
});
|
|
10385
10389
|
child.unref();
|
|
@@ -10391,7 +10395,7 @@ async function autoStartDaemonAndWait(openBrowser2 = true) {
|
|
|
10391
10395
|
signal: AbortSignal.timeout(500)
|
|
10392
10396
|
});
|
|
10393
10397
|
if (res.ok) {
|
|
10394
|
-
if (
|
|
10398
|
+
if (openBrowser) {
|
|
10395
10399
|
openBrowserLocal();
|
|
10396
10400
|
}
|
|
10397
10401
|
return true;
|
|
@@ -11779,370 +11783,681 @@ function printRuleGroup(rule, topN, drillDown, previewWidth) {
|
|
|
11779
11783
|
);
|
|
11780
11784
|
}
|
|
11781
11785
|
}
|
|
11782
|
-
function
|
|
11783
|
-
|
|
11784
|
-
|
|
11785
|
-
|
|
11786
|
-
|
|
11787
|
-
|
|
11788
|
-
|
|
11789
|
-
|
|
11790
|
-
|
|
11791
|
-
|
|
11792
|
-
})()
|
|
11793
|
-
|
|
11794
|
-
|
|
11795
|
-
|
|
11796
|
-
|
|
11797
|
-
|
|
11798
|
-
|
|
11799
|
-
|
|
11800
|
-
|
|
11801
|
-
|
|
11802
|
-
|
|
11786
|
+
function compactRuleLabel(name) {
|
|
11787
|
+
let label = name.replace(/^shield:[^:]+:/, "");
|
|
11788
|
+
label = label.replace(/^(block|review|allow)-/, "");
|
|
11789
|
+
return label.replace(/-+/g, "-");
|
|
11790
|
+
}
|
|
11791
|
+
function renderCompactScorecard(input) {
|
|
11792
|
+
const { scan, summary, blast, blastExposures, blockedCount, reviewCount } = input;
|
|
11793
|
+
const totalRisky = scan.findings.length + scan.dlpFindings.length;
|
|
11794
|
+
const dateRange = scan.firstDate && scan.lastDate ? `${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}` : "";
|
|
11795
|
+
console.log(
|
|
11796
|
+
chalk3.bold("\u{1F6E1} Node9 Scan") + chalk3.dim(" \xB7 ") + chalk3.white(num(scan.sessions)) + chalk3.dim(" sessions \xB7 ") + chalk3.white(num(scan.totalToolCalls)) + chalk3.dim(" tool calls") + (dateRange ? chalk3.dim(" \xB7 " + dateRange) : "")
|
|
11797
|
+
);
|
|
11798
|
+
console.log("");
|
|
11799
|
+
const scoreColor = blast.score >= 80 ? chalk3.green : blast.score >= 50 ? chalk3.yellow : chalk3.red;
|
|
11800
|
+
const scoreSeverity = blast.score >= 80 ? "Good" : blast.score >= 50 ? "At Risk" : "Critical";
|
|
11801
|
+
console.log(
|
|
11802
|
+
chalk3.bold("Security Score: ") + scoreColor.bold(`${blast.score}/100`) + chalk3.dim(" \xB7 ") + scoreColor(scoreSeverity)
|
|
11803
|
+
);
|
|
11804
|
+
if (scan.totalCostUSD > 0) {
|
|
11805
|
+
console.log(
|
|
11806
|
+
chalk3.bold(fmtCost(scan.totalCostUSD)) + chalk3.dim(" AI spend \xB7 ") + chalk3.bold(`${totalRisky}`) + chalk3.dim(` risky operation${totalRisky !== 1 ? "s" : ""}`)
|
|
11807
|
+
);
|
|
11808
|
+
}
|
|
11809
|
+
console.log("");
|
|
11810
|
+
if (scan.dlpFindings.length > 0) {
|
|
11811
|
+
const patternCounts = /* @__PURE__ */ new Map();
|
|
11812
|
+
for (const f of scan.dlpFindings) {
|
|
11813
|
+
patternCounts.set(f.patternName, (patternCounts.get(f.patternName) ?? 0) + 1);
|
|
11803
11814
|
}
|
|
11815
|
+
const topPatterns = [...patternCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([name, count]) => count > 1 ? `${name} \xD7${count}` : name).join(", ");
|
|
11804
11816
|
console.log(
|
|
11805
|
-
chalk3.
|
|
11817
|
+
chalk3.red("\u{1F511} ") + chalk3.red.bold(String(scan.dlpFindings.length).padEnd(4)) + chalk3.dim("credential leak".padEnd(20)) + chalk3.dim(`(${topPatterns})`)
|
|
11806
11818
|
);
|
|
11807
|
-
|
|
11808
|
-
|
|
11809
|
-
|
|
11810
|
-
|
|
11811
|
-
|
|
11812
|
-
|
|
11819
|
+
}
|
|
11820
|
+
if (blockedCount > 0) {
|
|
11821
|
+
const blockedRules = [];
|
|
11822
|
+
for (const section of summary.sections) {
|
|
11823
|
+
for (const rule of section.rules) {
|
|
11824
|
+
if (rule.verdict === "block") {
|
|
11825
|
+
blockedRules.push({ name: rule.name, count: rule.findings.length });
|
|
11826
|
+
}
|
|
11827
|
+
}
|
|
11813
11828
|
}
|
|
11814
|
-
const
|
|
11815
|
-
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
|
|
11819
|
-
|
|
11820
|
-
|
|
11821
|
-
|
|
11822
|
-
|
|
11823
|
-
const
|
|
11824
|
-
|
|
11825
|
-
|
|
11826
|
-
|
|
11827
|
-
|
|
11828
|
-
|
|
11829
|
+
const topBlocked = blockedRules.sort((a, b) => b.count - a.count).slice(0, 3).map(
|
|
11830
|
+
(r) => r.count > 1 ? `${compactRuleLabel(r.name)} \xD7${r.count}` : compactRuleLabel(r.name)
|
|
11831
|
+
).join(", ");
|
|
11832
|
+
console.log(
|
|
11833
|
+
chalk3.red("\u{1F6D1} ") + chalk3.red.bold(String(blockedCount).padEnd(4)) + chalk3.dim("would have blocked".padEnd(20)) + chalk3.dim(`(${topBlocked})`)
|
|
11834
|
+
);
|
|
11835
|
+
}
|
|
11836
|
+
if (scan.loopFindings.length > 0) {
|
|
11837
|
+
const wastedCalls = scan.loopFindings.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
|
|
11838
|
+
const wastePct = scan.totalToolCalls > 0 ? Math.round(wastedCalls / scan.totalToolCalls * 100) : 0;
|
|
11839
|
+
const wasteParts = [];
|
|
11840
|
+
if (wastePct > 0) wasteParts.push(`${wastePct}% wasted`);
|
|
11841
|
+
if (summary.loopWastedUSD > 0) wasteParts.push("~" + fmtCost(summary.loopWastedUSD));
|
|
11842
|
+
const wasteSummary = wasteParts.length ? `(${wasteParts.join(" \xB7 ")})` : "";
|
|
11843
|
+
console.log(
|
|
11844
|
+
chalk3.yellow("\u{1F501} ") + chalk3.yellow.bold(String(scan.loopFindings.length).padEnd(4)) + chalk3.dim("agent loops".padEnd(20)) + chalk3.dim(wasteSummary)
|
|
11845
|
+
);
|
|
11846
|
+
}
|
|
11847
|
+
if (reviewCount > 0) {
|
|
11848
|
+
const reviewRules = [];
|
|
11849
|
+
for (const section of summary.sections) {
|
|
11850
|
+
for (const rule of section.rules) {
|
|
11851
|
+
if (rule.verdict !== "block") {
|
|
11852
|
+
reviewRules.push({ name: rule.name, count: rule.findings.length });
|
|
11853
|
+
}
|
|
11829
11854
|
}
|
|
11830
|
-
}
|
|
11831
|
-
|
|
11832
|
-
|
|
11833
|
-
|
|
11834
|
-
|
|
11835
|
-
(
|
|
11836
|
-
onLine
|
|
11855
|
+
}
|
|
11856
|
+
const topReview = reviewRules.sort((a, b) => b.count - a.count).slice(0, 3).map(
|
|
11857
|
+
(r) => r.count > 1 ? `${compactRuleLabel(r.name)} \xD7${r.count}` : compactRuleLabel(r.name)
|
|
11858
|
+
).join(", ");
|
|
11859
|
+
console.log(
|
|
11860
|
+
chalk3.yellow("\u{1F441} ") + chalk3.yellow.bold(String(reviewCount).padEnd(4)) + chalk3.dim("flagged for review".padEnd(20)) + chalk3.dim(`(${topReview})`)
|
|
11837
11861
|
);
|
|
11838
|
-
|
|
11839
|
-
|
|
11840
|
-
|
|
11841
|
-
|
|
11862
|
+
}
|
|
11863
|
+
console.log("");
|
|
11864
|
+
if (blastExposures > 0) {
|
|
11865
|
+
const categories = /* @__PURE__ */ new Set();
|
|
11866
|
+
for (const r of blast.reachable) {
|
|
11867
|
+
const lower = r.label.toLowerCase();
|
|
11868
|
+
if (lower.includes("ssh")) categories.add("ssh");
|
|
11869
|
+
else if (lower.includes("aws")) categories.add("aws");
|
|
11870
|
+
else if (lower.includes("gcloud") || lower.includes("gcp")) categories.add("gcp");
|
|
11871
|
+
else if (lower.includes("docker")) categories.add("docker");
|
|
11872
|
+
else if (lower.includes("netrc")) categories.add("netrc");
|
|
11873
|
+
else if (lower.includes("kube")) categories.add("k8s");
|
|
11874
|
+
else if (lower.includes("npmrc")) categories.add("npm");
|
|
11875
|
+
else categories.add("other");
|
|
11876
|
+
}
|
|
11877
|
+
if (blast.envFindings.length > 0) categories.add("env");
|
|
11878
|
+
const catList = [...categories].slice(0, 6).join(" \xD7 ");
|
|
11879
|
+
console.log(
|
|
11880
|
+
chalk3.red("\u{1F52D} ") + chalk3.dim("Blast radius".padEnd(24)) + chalk3.dim(`${catList} (${blastExposures} exposure${blastExposures !== 1 ? "s" : ""})`)
|
|
11842
11881
|
);
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
|
|
11850
|
-
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
|
|
11857
|
-
|
|
11858
|
-
|
|
11882
|
+
console.log("");
|
|
11883
|
+
}
|
|
11884
|
+
console.log(
|
|
11885
|
+
chalk3.dim("\u2192 ") + chalk3.cyan("npx node9-ai scan") + chalk3.dim(" run this on your machine")
|
|
11886
|
+
);
|
|
11887
|
+
console.log(chalk3.dim("\u2192 github.com/node9-ai/node9-proxy"));
|
|
11888
|
+
console.log("");
|
|
11889
|
+
}
|
|
11890
|
+
function classifyRuleSeverity(name, verdict) {
|
|
11891
|
+
const n = name.toLowerCase();
|
|
11892
|
+
const criticalPatterns = [
|
|
11893
|
+
"rm-rf",
|
|
11894
|
+
"eval-remote",
|
|
11895
|
+
"eval-curl",
|
|
11896
|
+
"read-aws",
|
|
11897
|
+
"read-ssh",
|
|
11898
|
+
"read-gcp",
|
|
11899
|
+
"read-cred",
|
|
11900
|
+
"delete-repo",
|
|
11901
|
+
"helm-uninstall",
|
|
11902
|
+
"drop-table",
|
|
11903
|
+
"drop-database",
|
|
11904
|
+
"drop-collection",
|
|
11905
|
+
"truncate",
|
|
11906
|
+
"flushall",
|
|
11907
|
+
"flushdb",
|
|
11908
|
+
"pipe-shell"
|
|
11909
|
+
];
|
|
11910
|
+
if (criticalPatterns.some((p) => n.includes(p))) return "critical";
|
|
11911
|
+
const highPatterns = [
|
|
11912
|
+
"force-push",
|
|
11913
|
+
"force_push",
|
|
11914
|
+
"git-destructive",
|
|
11915
|
+
"reset-hard",
|
|
11916
|
+
"rebase",
|
|
11917
|
+
"delete-branch",
|
|
11918
|
+
"delete-remote"
|
|
11919
|
+
];
|
|
11920
|
+
if (highPatterns.some((p) => n.includes(p))) return "high";
|
|
11921
|
+
if (verdict === "block") return "high";
|
|
11922
|
+
return "medium";
|
|
11923
|
+
}
|
|
11924
|
+
function narrativeRuleLabel(name) {
|
|
11925
|
+
const stripped = compactRuleLabel(name);
|
|
11926
|
+
const map = {
|
|
11927
|
+
"read-aws": "AWS credentials read",
|
|
11928
|
+
"read-ssh": "SSH private key read",
|
|
11929
|
+
"read-gcp": "GCP credentials read",
|
|
11930
|
+
"read-cred": "credential file read",
|
|
11931
|
+
"delete-repo": "GitHub repository deletion",
|
|
11932
|
+
"helm-uninstall": "helm uninstall",
|
|
11933
|
+
"rm-rf-home": "rm -rf on home directory",
|
|
11934
|
+
"eval-remote": "eval of remote download",
|
|
11935
|
+
"pipe-shell": "curl | bash",
|
|
11936
|
+
"drop-table": "DROP TABLE",
|
|
11937
|
+
"drop-database": "DROP DATABASE",
|
|
11938
|
+
truncate: "TRUNCATE",
|
|
11939
|
+
flushall: "Redis FLUSHALL",
|
|
11940
|
+
flushdb: "Redis FLUSHDB",
|
|
11941
|
+
"force-push": "force pushes",
|
|
11942
|
+
"git-destructive": "destructive git operations",
|
|
11943
|
+
rm: "rm calls",
|
|
11944
|
+
sudo: "sudo calls",
|
|
11945
|
+
"eval-dynamic": "dynamic eval",
|
|
11946
|
+
"config-set": "Redis CONFIG SET"
|
|
11947
|
+
};
|
|
11948
|
+
for (const [key, label] of Object.entries(map)) {
|
|
11949
|
+
if (stripped.includes(key)) return label;
|
|
11950
|
+
}
|
|
11951
|
+
return stripped;
|
|
11952
|
+
}
|
|
11953
|
+
function renderNarrativeScorecard(input) {
|
|
11954
|
+
const { scan, summary, blast, blastExposures } = input;
|
|
11955
|
+
const critical = [];
|
|
11956
|
+
const high = [];
|
|
11957
|
+
const medium = [];
|
|
11958
|
+
if (scan.dlpFindings.length > 0) {
|
|
11959
|
+
const patterns = /* @__PURE__ */ new Map();
|
|
11960
|
+
for (const f of scan.dlpFindings) {
|
|
11961
|
+
patterns.set(f.patternName, (patterns.get(f.patternName) ?? 0) + 1);
|
|
11962
|
+
}
|
|
11963
|
+
const top = [...patterns.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([name, n]) => n > 1 ? `${name} \xD7${n}` : name).join(", ");
|
|
11964
|
+
critical.push({
|
|
11965
|
+
label: `${scan.dlpFindings.length} credential leak${scan.dlpFindings.length !== 1 ? "s" : ""} (${top})`,
|
|
11966
|
+
count: scan.dlpFindings.length
|
|
11967
|
+
});
|
|
11968
|
+
}
|
|
11969
|
+
for (const section of summary.sections) {
|
|
11970
|
+
for (const rule of section.rules) {
|
|
11971
|
+
const sev = classifyRuleSeverity(rule.name, rule.verdict);
|
|
11972
|
+
const label = narrativeRuleLabel(rule.name);
|
|
11973
|
+
const count = rule.findings.length;
|
|
11974
|
+
const display = count > 1 ? `${label} \xD7${count}` : label;
|
|
11975
|
+
const entry = { label: display, count };
|
|
11976
|
+
if (sev === "critical") critical.push(entry);
|
|
11977
|
+
else if (sev === "high") high.push(entry);
|
|
11978
|
+
else medium.push(entry);
|
|
11979
|
+
}
|
|
11980
|
+
}
|
|
11981
|
+
if (blastExposures > 0) {
|
|
11982
|
+
high.push({
|
|
11983
|
+
label: `${blastExposures} credential file${blastExposures !== 1 ? "s" : ""} reachable on disk`,
|
|
11984
|
+
count: blastExposures
|
|
11985
|
+
});
|
|
11986
|
+
}
|
|
11987
|
+
if (scan.loopFindings.length > 0) {
|
|
11988
|
+
const wastedCalls = scan.loopFindings.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
|
|
11989
|
+
const wastePct = scan.totalToolCalls > 0 ? Math.round(wastedCalls / scan.totalToolCalls * 100) : 0;
|
|
11990
|
+
const cost = summary.loopWastedUSD > 0 ? `, ~${fmtCost(summary.loopWastedUSD)} wasted` : "";
|
|
11991
|
+
medium.push({
|
|
11992
|
+
label: `${scan.loopFindings.length} agent loops (${wastePct}% of calls${cost})`,
|
|
11993
|
+
count: scan.loopFindings.length
|
|
11994
|
+
});
|
|
11995
|
+
}
|
|
11996
|
+
const sortByCount = (a, b) => b.count - a.count;
|
|
11997
|
+
critical.sort(sortByCount);
|
|
11998
|
+
high.sort(sortByCount);
|
|
11999
|
+
medium.sort(sortByCount);
|
|
12000
|
+
const criticalCount = critical.reduce((s, e) => s + e.count, 0);
|
|
12001
|
+
const highCount = high.reduce((s, e) => s + e.count, 0);
|
|
12002
|
+
const mediumCount = medium.reduce((s, e) => s + e.count, 0);
|
|
12003
|
+
const dateRange = scan.firstDate && scan.lastDate ? `${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}` : "";
|
|
12004
|
+
console.log(
|
|
12005
|
+
chalk3.bold("\u{1F6E1} Node9 Scan") + chalk3.dim(" \xB7 ") + chalk3.white(num(scan.sessions)) + chalk3.dim(" sessions") + (scan.totalCostUSD > 0 ? chalk3.dim(" \xB7 ") + chalk3.bold(fmtCost(scan.totalCostUSD)) + chalk3.dim(" spend") : "") + (dateRange ? chalk3.dim(" \xB7 " + dateRange) : "")
|
|
12006
|
+
);
|
|
12007
|
+
console.log("");
|
|
12008
|
+
const scoreColor = blast.score >= 80 ? chalk3.green : blast.score >= 50 ? chalk3.yellow : chalk3.red;
|
|
12009
|
+
const scoreSeverity = blast.score >= 80 ? "Good" : blast.score >= 50 ? "At Risk" : "Critical";
|
|
12010
|
+
console.log(
|
|
12011
|
+
(blast.score < 50 ? chalk3.red.bold("\u26A0 ") : "") + chalk3.bold("Security Score: ") + scoreColor.bold(`${blast.score}/100`) + chalk3.dim(" \xB7 ") + scoreColor(scoreSeverity)
|
|
12012
|
+
);
|
|
12013
|
+
console.log("");
|
|
12014
|
+
if (criticalCount > 0) {
|
|
12015
|
+
console.log(
|
|
12016
|
+
chalk3.red.bold(" \u{1F534} CRITICAL ") + chalk3.red(`${criticalCount} finding${criticalCount !== 1 ? "s" : ""}`)
|
|
12017
|
+
);
|
|
12018
|
+
for (const entry of critical.slice(0, 5)) {
|
|
12019
|
+
console.log(chalk3.dim(" \u2022 ") + chalk3.red(entry.label));
|
|
11859
12020
|
}
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
if (codexScan.sessions > 0)
|
|
11868
|
-
breakdownParts.push(chalk3.magenta(String(codexScan.sessions)) + chalk3.dim(" Codex"));
|
|
11869
|
-
const sessionBreakdown = breakdownParts.length > 1 ? chalk3.dim("(") + breakdownParts.join(chalk3.dim(" \xB7 ")) + chalk3.dim(")") : "";
|
|
12021
|
+
if (critical.length > 5) {
|
|
12022
|
+
const remaining = critical.length - 5;
|
|
12023
|
+
console.log(chalk3.dim(` \u2022 \u2026 and ${remaining} more`));
|
|
12024
|
+
}
|
|
12025
|
+
console.log("");
|
|
12026
|
+
}
|
|
12027
|
+
if (highCount > 0) {
|
|
11870
12028
|
console.log(
|
|
11871
|
-
|
|
12029
|
+
chalk3.yellow.bold(" \u{1F7E1} HIGH ") + chalk3.yellow(`${highCount} finding${highCount !== 1 ? "s" : ""}`)
|
|
11872
12030
|
);
|
|
12031
|
+
for (const entry of high.slice(0, 5)) {
|
|
12032
|
+
console.log(chalk3.dim(" \u2022 ") + chalk3.yellow(entry.label));
|
|
12033
|
+
}
|
|
12034
|
+
if (high.length > 5) {
|
|
12035
|
+
const remaining = high.length - 5;
|
|
12036
|
+
console.log(chalk3.dim(` \u2022 \u2026 and ${remaining} more`));
|
|
12037
|
+
}
|
|
11873
12038
|
console.log("");
|
|
11874
|
-
|
|
11875
|
-
|
|
11876
|
-
|
|
11877
|
-
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
const
|
|
11884
|
-
|
|
11885
|
-
|
|
11886
|
-
|
|
11887
|
-
|
|
11888
|
-
|
|
11889
|
-
|
|
11890
|
-
|
|
11891
|
-
|
|
12039
|
+
}
|
|
12040
|
+
if (mediumCount > 0) {
|
|
12041
|
+
console.log(
|
|
12042
|
+
chalk3.bold(" \u{1F7E2} MEDIUM ") + chalk3.dim(`${mediumCount} finding${mediumCount !== 1 ? "s" : ""}`)
|
|
12043
|
+
);
|
|
12044
|
+
for (const entry of medium.slice(0, 5)) {
|
|
12045
|
+
console.log(chalk3.dim(" \u2022 ") + chalk3.dim(entry.label));
|
|
12046
|
+
}
|
|
12047
|
+
if (medium.length > 5) {
|
|
12048
|
+
const remaining = medium.length - 5;
|
|
12049
|
+
console.log(chalk3.dim(` \u2022 \u2026 and ${remaining} more`));
|
|
12050
|
+
}
|
|
12051
|
+
console.log("");
|
|
12052
|
+
}
|
|
12053
|
+
console.log(
|
|
12054
|
+
chalk3.dim("\u2192 ") + chalk3.cyan("npx node9-ai scan") + chalk3.dim(" run this on your machine")
|
|
12055
|
+
);
|
|
12056
|
+
console.log(chalk3.dim("\u2192 github.com/node9-ai/node9-proxy"));
|
|
12057
|
+
console.log("");
|
|
12058
|
+
}
|
|
12059
|
+
function registerScanCommand(program2) {
|
|
12060
|
+
program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 30 days)").option("--days <n>", "Scan last N days of history", "30").option("--top <n>", "Max findings to show per rule (default: 5)", "5").option("--drill-down", "Show all findings with full commands and session IDs").option("--compact", "Compact one-screen scorecard \u2014 for screenshots and sharing").option("--narrative", "Severity-grouped report \u2014 for video / dramatic sharing").action(
|
|
12061
|
+
async (options) => {
|
|
12062
|
+
const drillDown = options.drillDown ?? false;
|
|
12063
|
+
const topN = drillDown ? Infinity : Math.max(1, parseInt(options.top, 10) || 5);
|
|
12064
|
+
const previewWidth = 70;
|
|
12065
|
+
const startDate = options.all ? null : (() => {
|
|
12066
|
+
const d = /* @__PURE__ */ new Date();
|
|
12067
|
+
d.setDate(d.getDate() - (parseInt(options.days, 10) || 30));
|
|
12068
|
+
d.setHours(0, 0, 0, 0);
|
|
12069
|
+
return d;
|
|
12070
|
+
})();
|
|
12071
|
+
const isWired = getAgentsStatus().some((a) => a.wired);
|
|
12072
|
+
const screenshotMode = options.compact || options.narrative;
|
|
12073
|
+
if (!screenshotMode) {
|
|
12074
|
+
console.log("");
|
|
12075
|
+
if (!isWired) {
|
|
12076
|
+
console.log(
|
|
12077
|
+
chalk3.bold("\u{1F6E1} node9") + chalk3.dim(" \u2014 security layer for AI coding agents")
|
|
12078
|
+
);
|
|
12079
|
+
console.log(
|
|
12080
|
+
chalk3.dim(" Intercepts dangerous tool calls before they execute. No config needed.")
|
|
12081
|
+
);
|
|
12082
|
+
console.log("");
|
|
12083
|
+
}
|
|
11892
12084
|
console.log(
|
|
11893
|
-
|
|
12085
|
+
chalk3.cyan.bold("\u{1F50D} Scanning your AI history") + chalk3.dim(" \u2014 what would node9 have caught?")
|
|
11894
12086
|
);
|
|
12087
|
+
console.log("");
|
|
11895
12088
|
}
|
|
11896
|
-
|
|
11897
|
-
|
|
11898
|
-
|
|
11899
|
-
|
|
11900
|
-
console.log(
|
|
11901
|
-
" " + chalk3.red("\u{1F511} Credential leak") + " " + chalk3.red.bold(String(scan.dlpFindings.length).padStart(5)) + chalk3.dim(" secret detected in history or shell config") + earlyLabel
|
|
12089
|
+
const useTTY = process.stdout.isTTY === true && process.env.NODE9_WRAPPER !== "1";
|
|
12090
|
+
if (!useTTY && !screenshotMode) {
|
|
12091
|
+
process.stdout.write(
|
|
12092
|
+
" " + chalk3.dim("Scanning your history \u2014 this may take a moment...\n")
|
|
11902
12093
|
);
|
|
11903
12094
|
}
|
|
11904
|
-
|
|
12095
|
+
const totalFiles = countScanFiles();
|
|
12096
|
+
let filesScanned = 0;
|
|
12097
|
+
let linesScanned = 0;
|
|
12098
|
+
let lastRender = 0;
|
|
12099
|
+
const onProgress = (done) => {
|
|
12100
|
+
filesScanned = done;
|
|
12101
|
+
if (useTTY) renderProgressBar(filesScanned, totalFiles, linesScanned);
|
|
12102
|
+
lastRender = Date.now();
|
|
12103
|
+
};
|
|
12104
|
+
const onLine = () => {
|
|
12105
|
+
linesScanned++;
|
|
12106
|
+
const now = Date.now();
|
|
12107
|
+
if (useTTY && now - lastRender >= 80) {
|
|
12108
|
+
lastRender = now;
|
|
12109
|
+
renderProgressBar(filesScanned, totalFiles, linesScanned);
|
|
12110
|
+
}
|
|
12111
|
+
};
|
|
12112
|
+
if (useTTY) renderProgressBar(0, totalFiles, 0);
|
|
12113
|
+
const claudeScan = scanClaudeHistory(startDate, onProgress, onLine);
|
|
12114
|
+
const geminiScan = scanGeminiHistory(
|
|
12115
|
+
startDate,
|
|
12116
|
+
(done) => onProgress(claudeScan.filesScanned + done),
|
|
12117
|
+
onLine
|
|
12118
|
+
);
|
|
12119
|
+
const codexScan = scanCodexHistory(
|
|
12120
|
+
startDate,
|
|
12121
|
+
(done) => onProgress(claudeScan.filesScanned + geminiScan.filesScanned + done),
|
|
12122
|
+
onLine
|
|
12123
|
+
);
|
|
12124
|
+
const scan = mergeScans(mergeScans(claudeScan, geminiScan), codexScan);
|
|
12125
|
+
scan.dlpFindings.push(...scanShellConfig());
|
|
12126
|
+
const summary = buildScanSummary([
|
|
12127
|
+
{ id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
|
|
12128
|
+
{ id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
|
|
12129
|
+
{ id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
|
|
12130
|
+
]);
|
|
12131
|
+
if (useTTY) process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
12132
|
+
if (scan.filesScanned === 0) {
|
|
12133
|
+
console.log(chalk3.yellow(" No session history found."));
|
|
11905
12134
|
console.log(
|
|
11906
|
-
|
|
12135
|
+
chalk3.gray(
|
|
12136
|
+
" Supported: Claude Code (~/.claude/projects/) \xB7 Gemini CLI (~/.gemini/tmp/)\n"
|
|
12137
|
+
)
|
|
11907
12138
|
);
|
|
12139
|
+
return;
|
|
11908
12140
|
}
|
|
11909
|
-
|
|
11910
|
-
|
|
11911
|
-
|
|
11912
|
-
|
|
11913
|
-
)
|
|
11914
|
-
|
|
12141
|
+
const rangeLabel = options.all ? chalk3.dim("all time") : chalk3.dim(`last ${options.days ?? 30} days`);
|
|
12142
|
+
const dateRange = scan.firstDate && scan.lastDate ? chalk3.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
|
|
12143
|
+
const breakdownParts = [];
|
|
12144
|
+
if (claudeScan.sessions > 0)
|
|
12145
|
+
breakdownParts.push(chalk3.cyan(String(claudeScan.sessions)) + chalk3.dim(" Claude"));
|
|
12146
|
+
if (geminiScan.sessions > 0)
|
|
12147
|
+
breakdownParts.push(chalk3.blue(String(geminiScan.sessions)) + chalk3.dim(" Gemini"));
|
|
12148
|
+
if (codexScan.sessions > 0)
|
|
12149
|
+
breakdownParts.push(chalk3.magenta(String(codexScan.sessions)) + chalk3.dim(" Codex"));
|
|
12150
|
+
const sessionBreakdown = breakdownParts.length > 1 ? chalk3.dim("(") + breakdownParts.join(chalk3.dim(" \xB7 ")) + chalk3.dim(")") : "";
|
|
12151
|
+
if (!screenshotMode) {
|
|
11915
12152
|
console.log(
|
|
11916
|
-
"
|
|
12153
|
+
" " + chalk3.white(num(scan.sessions)) + chalk3.dim(" sessions ") + sessionBreakdown + (sessionBreakdown ? " " : "") + chalk3.white(num(scan.totalToolCalls)) + chalk3.dim(" tool calls ") + chalk3.white(num(scan.bashCalls)) + chalk3.dim(" bash commands ") + rangeLabel + dateRange
|
|
11917
12154
|
);
|
|
12155
|
+
console.log("");
|
|
11918
12156
|
}
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
12157
|
+
const totalFindings = scan.findings.length;
|
|
12158
|
+
const blockedCount = scan.findings.filter((f) => f.source.rule.verdict === "block").length;
|
|
12159
|
+
const reviewCount = totalFindings - blockedCount;
|
|
12160
|
+
const blast = runBlast();
|
|
12161
|
+
const blastExposures = blast.reachable.length + blast.envFindings.length;
|
|
12162
|
+
if (options.compact) {
|
|
12163
|
+
renderCompactScorecard({
|
|
12164
|
+
scan,
|
|
12165
|
+
summary,
|
|
12166
|
+
blast,
|
|
12167
|
+
blastExposures,
|
|
12168
|
+
blockedCount,
|
|
12169
|
+
reviewCount
|
|
12170
|
+
});
|
|
12171
|
+
return;
|
|
11923
12172
|
}
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
12173
|
+
if (options.narrative) {
|
|
12174
|
+
renderNarrativeScorecard({
|
|
12175
|
+
scan,
|
|
12176
|
+
summary,
|
|
12177
|
+
blast,
|
|
12178
|
+
blastExposures,
|
|
12179
|
+
blockedCount,
|
|
12180
|
+
reviewCount
|
|
12181
|
+
});
|
|
12182
|
+
return;
|
|
12183
|
+
}
|
|
12184
|
+
if (totalFindings === 0 && scan.dlpFindings.length === 0) {
|
|
12185
|
+
console.log(chalk3.green(" \u2705 No risky operations found in your history."));
|
|
11927
12186
|
console.log(
|
|
11928
|
-
|
|
11929
|
-
|
|
12187
|
+
chalk3.dim(
|
|
12188
|
+
" node9 is still worth running \u2014 it monitors every tool call in real time.\n"
|
|
11930
12189
|
)
|
|
11931
12190
|
);
|
|
11932
|
-
|
|
11933
|
-
const
|
|
11934
|
-
const
|
|
11935
|
-
|
|
11936
|
-
|
|
11937
|
-
|
|
11938
|
-
|
|
11939
|
-
|
|
11940
|
-
|
|
11941
|
-
|
|
11942
|
-
|
|
11943
|
-
|
|
11944
|
-
|
|
11945
|
-
|
|
12191
|
+
} else {
|
|
12192
|
+
const totalRisky = totalFindings + scan.dlpFindings.length;
|
|
12193
|
+
const scoreSeverity = blast.score >= 80 ? chalk3.green("Good") : blast.score >= 50 ? chalk3.yellow("At Risk") : chalk3.red.bold("Critical");
|
|
12194
|
+
const scoreColor = blast.score >= 80 ? chalk3.green : blast.score >= 50 ? chalk3.yellow : chalk3.red;
|
|
12195
|
+
console.log(
|
|
12196
|
+
" " + (blast.score < 50 ? chalk3.red.bold("\u26A0 ") : "") + chalk3.bold("Security Score ") + scoreColor.bold(`${blast.score}/100`) + " " + scoreSeverity + chalk3.dim(" \xB7 ") + (totalRisky > 0 ? chalk3.red.bold(`${totalRisky} risky operation${totalRisky !== 1 ? "s" : ""}`) : chalk3.green("No risky operations"))
|
|
12197
|
+
);
|
|
12198
|
+
const cardParts = [];
|
|
12199
|
+
if (scan.dlpFindings.length > 0) {
|
|
12200
|
+
cardParts.push(
|
|
12201
|
+
chalk3.red("\u{1F511} ") + chalk3.red.bold(String(scan.dlpFindings.length)) + chalk3.dim(` leak${scan.dlpFindings.length !== 1 ? "s" : ""}`)
|
|
12202
|
+
);
|
|
12203
|
+
}
|
|
12204
|
+
if (blockedCount > 0) {
|
|
12205
|
+
cardParts.push(
|
|
12206
|
+
chalk3.red("\u{1F6D1} ") + chalk3.red.bold(String(blockedCount)) + chalk3.dim(" blocked")
|
|
12207
|
+
);
|
|
12208
|
+
}
|
|
12209
|
+
if (scan.loopFindings.length > 0) {
|
|
12210
|
+
const wastedCalls = scan.loopFindings.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
|
|
12211
|
+
const wastePct = scan.totalToolCalls > 0 ? Math.round(wastedCalls / scan.totalToolCalls * 100) : 0;
|
|
12212
|
+
const wasteSuffix = wastePct > 0 ? chalk3.dim(` (${wastePct}% wasted)`) : "";
|
|
12213
|
+
cardParts.push(
|
|
12214
|
+
chalk3.yellow("\u{1F501} ") + chalk3.yellow.bold(String(scan.loopFindings.length)) + chalk3.dim(" loops") + wasteSuffix
|
|
12215
|
+
);
|
|
12216
|
+
}
|
|
12217
|
+
if (reviewCount > 0) {
|
|
12218
|
+
cardParts.push(
|
|
12219
|
+
chalk3.yellow("\u{1F441} ") + chalk3.yellow.bold(String(reviewCount)) + chalk3.dim(" flagged")
|
|
12220
|
+
);
|
|
12221
|
+
}
|
|
12222
|
+
if (blastExposures > 0) {
|
|
12223
|
+
cardParts.push(
|
|
12224
|
+
chalk3.red("\u{1F52D} ") + chalk3.red.bold(String(blastExposures)) + chalk3.dim(" exposures")
|
|
12225
|
+
);
|
|
12226
|
+
}
|
|
12227
|
+
if (cardParts.length > 0) {
|
|
12228
|
+
console.log(" " + cardParts.join(chalk3.dim(" ")));
|
|
12229
|
+
}
|
|
12230
|
+
if (scan.totalCostUSD > 0) {
|
|
11946
12231
|
console.log(
|
|
11947
|
-
|
|
12232
|
+
" " + chalk3.dim("AI spend ") + chalk3.bold(fmtCost(scan.totalCostUSD)) + (summary.loopWastedUSD > 0 ? chalk3.dim(" \xB7 wasted on loops ") + chalk3.yellow("~" + fmtCost(summary.loopWastedUSD)) : "")
|
|
11948
12233
|
);
|
|
11949
12234
|
}
|
|
11950
|
-
if (
|
|
12235
|
+
if (scan.dlpFindings.length > 0 && scan.sessionsWithEarlySecrets > 0) {
|
|
11951
12236
|
console.log(
|
|
11952
|
-
chalk3.dim(
|
|
11953
|
-
|
|
12237
|
+
" " + chalk3.dim(
|
|
12238
|
+
`${scan.sessionsWithEarlySecrets} session${scan.sessionsWithEarlySecrets !== 1 ? "s" : ""} loaded secrets before first edit`
|
|
11954
12239
|
)
|
|
11955
12240
|
);
|
|
11956
12241
|
}
|
|
11957
12242
|
console.log("");
|
|
11958
|
-
|
|
11959
|
-
|
|
11960
|
-
if (blockedRuleSections.length > 0) {
|
|
11961
|
-
console.log(" " + chalk3.dim("\u2500".repeat(70)));
|
|
11962
|
-
console.log(
|
|
11963
|
-
" " + chalk3.red.bold("\u{1F6D1} Blocked") + chalk3.dim(" \xB7 ") + chalk3.red(
|
|
11964
|
-
`${blockedCount} operation${blockedCount !== 1 ? "s" : ""} node9 would have stopped`
|
|
11965
|
-
)
|
|
11966
|
-
);
|
|
11967
|
-
for (const section of blockedRuleSections) {
|
|
11968
|
-
for (const rule of section.rules) {
|
|
11969
|
-
printRuleGroup(rule, topN, drillDown, previewWidth);
|
|
11970
|
-
}
|
|
11971
|
-
}
|
|
11972
|
-
console.log("");
|
|
11973
|
-
}
|
|
11974
|
-
if (scan.loopFindings.length > 0) {
|
|
11975
|
-
console.log(" " + chalk3.dim("\u2500".repeat(70)));
|
|
11976
|
-
const loopCostLabel = summary.loopWastedUSD > 0 ? chalk3.dim(" \xB7 ") + chalk3.yellow("~" + fmtCost(summary.loopWastedUSD) + " wasted") : "";
|
|
11977
|
-
console.log(
|
|
11978
|
-
" " + chalk3.yellow.bold("\u{1F501} Agent Loops") + chalk3.dim(" \xB7 ") + chalk3.yellow(
|
|
11979
|
-
`${num(scan.loopFindings.length)} repeated pattern${scan.loopFindings.length !== 1 ? "s" : ""} found`
|
|
11980
|
-
) + loopCostLabel
|
|
11981
|
-
);
|
|
11982
|
-
const shownLoops = drillDown ? scan.loopFindings : scan.loopFindings.slice(0, topN);
|
|
11983
|
-
for (const f of shownLoops) {
|
|
11984
|
-
const ts = f.timestamp ? chalk3.dim(fmtTs(f.timestamp) + " ") : "";
|
|
11985
|
-
const proj = chalk3.dim(f.project.slice(0, 22).padEnd(22) + " ");
|
|
11986
|
-
const agentBadge = f.agent === "gemini" ? chalk3.blue("[Gemini] ") : f.agent === "codex" ? chalk3.magenta("[Codex] ") : chalk3.cyan("[Claude] ");
|
|
11987
|
-
const sessionSuffix = f.sessionId ? chalk3.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
|
|
12243
|
+
if (scan.dlpFindings.length > 0) {
|
|
12244
|
+
console.log(" " + chalk3.dim("\u2500".repeat(70)));
|
|
11988
12245
|
console.log(
|
|
11989
|
-
|
|
12246
|
+
" " + chalk3.red.bold("\u{1F511} Credential Leaks") + chalk3.dim(" \xB7 ") + chalk3.red(
|
|
12247
|
+
`${num(scan.dlpFindings.length)} secret${scan.dlpFindings.length !== 1 ? "s" : ""} found in plain text`
|
|
12248
|
+
)
|
|
11990
12249
|
);
|
|
12250
|
+
const sortedDlp = sortDlpFindingsByPriority(scan.dlpFindings);
|
|
12251
|
+
const recurringPatterns = buildRecurringPatternSet(scan.dlpFindings);
|
|
12252
|
+
const shownDlp = drillDown ? sortedDlp : sortedDlp.slice(0, topN);
|
|
12253
|
+
for (const f of shownDlp) {
|
|
12254
|
+
const stale = isStaleFinding(f.timestamp);
|
|
12255
|
+
const ts = f.timestamp ? chalk3.dim(fmtTs(f.timestamp) + " ") : "";
|
|
12256
|
+
const proj = chalk3.dim(f.project.slice(0, 22).padEnd(22) + " ");
|
|
12257
|
+
const agentBadge = f.agent === "gemini" ? chalk3.blue("[Gemini] ") : f.agent === "codex" ? chalk3.magenta("[Codex] ") : f.agent === "shell" ? chalk3.yellow("[Shell] ") : chalk3.cyan("[Claude] ");
|
|
12258
|
+
const sessionSuffix = f.sessionId ? chalk3.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
|
|
12259
|
+
const recurringBadge = recurringPatterns.has(f.patternName) ? chalk3.red.bold(" \u26A0\uFE0F recurring ") : "";
|
|
12260
|
+
const patternDisplay = stale ? chalk3.dim(f.patternName) : chalk3.yellow(f.patternName);
|
|
12261
|
+
const sampleDisplay = stale ? chalk3.dim(f.redactedSample) : chalk3.gray(f.redactedSample);
|
|
12262
|
+
const entryBadge = chalk3.dim(` [${entryPathLabel(f.toolName)}]`);
|
|
12263
|
+
const leadIcon = stale ? chalk3.dim("\u{1F6A8}") : "\u{1F6A8}";
|
|
12264
|
+
console.log(
|
|
12265
|
+
` ${leadIcon} ${ts}${proj}${agentBadge}` + patternDisplay + recurringBadge + chalk3.dim(" ") + sampleDisplay + entryBadge + sessionSuffix
|
|
12266
|
+
);
|
|
12267
|
+
}
|
|
12268
|
+
if (!drillDown && scan.dlpFindings.length > topN) {
|
|
12269
|
+
console.log(
|
|
12270
|
+
chalk3.dim(
|
|
12271
|
+
` \u2026 and ${scan.dlpFindings.length - topN} more (--drill-down for full list)`
|
|
12272
|
+
)
|
|
12273
|
+
);
|
|
12274
|
+
}
|
|
12275
|
+
console.log("");
|
|
11991
12276
|
}
|
|
11992
|
-
|
|
12277
|
+
const blockedRuleSections = summary.sections.map((s) => ({ ...s, rules: s.rules.filter((r) => r.verdict === "block") })).filter((s) => s.rules.length > 0);
|
|
12278
|
+
if (blockedRuleSections.length > 0) {
|
|
12279
|
+
console.log(" " + chalk3.dim("\u2500".repeat(70)));
|
|
11993
12280
|
console.log(
|
|
11994
|
-
chalk3.dim(
|
|
11995
|
-
|
|
12281
|
+
" " + chalk3.red.bold("\u{1F6D1} Blocked") + chalk3.dim(" \xB7 ") + chalk3.red(
|
|
12282
|
+
`${blockedCount} operation${blockedCount !== 1 ? "s" : ""} node9 would have stopped`
|
|
11996
12283
|
)
|
|
11997
12284
|
);
|
|
11998
|
-
|
|
11999
|
-
|
|
12000
|
-
|
|
12285
|
+
for (const section of blockedRuleSections) {
|
|
12286
|
+
for (const rule of section.rules) {
|
|
12287
|
+
printRuleGroup(rule, topN, drillDown, previewWidth);
|
|
12288
|
+
}
|
|
12289
|
+
}
|
|
12001
12290
|
console.log("");
|
|
12002
|
-
|
|
12003
|
-
|
|
12291
|
+
}
|
|
12292
|
+
if (scan.loopFindings.length > 0) {
|
|
12293
|
+
console.log(" " + chalk3.dim("\u2500".repeat(70)));
|
|
12294
|
+
const loopCostLabel = summary.loopWastedUSD > 0 ? chalk3.dim(" \xB7 ") + chalk3.yellow("~" + fmtCost(summary.loopWastedUSD) + " wasted") : "";
|
|
12295
|
+
console.log(
|
|
12296
|
+
" " + chalk3.yellow.bold("\u{1F501} Agent Loops") + chalk3.dim(" \xB7 ") + chalk3.yellow(
|
|
12297
|
+
`${num(scan.loopFindings.length)} repeated pattern${scan.loopFindings.length !== 1 ? "s" : ""} found`
|
|
12298
|
+
) + loopCostLabel
|
|
12299
|
+
);
|
|
12300
|
+
const shownLoops = drillDown ? scan.loopFindings : scan.loopFindings.slice(0, topN);
|
|
12301
|
+
for (const f of shownLoops) {
|
|
12302
|
+
const ts = f.timestamp ? chalk3.dim(fmtTs(f.timestamp) + " ") : "";
|
|
12303
|
+
const proj = chalk3.dim(f.project.slice(0, 22).padEnd(22) + " ");
|
|
12304
|
+
const agentBadge = f.agent === "gemini" ? chalk3.blue("[Gemini] ") : f.agent === "codex" ? chalk3.magenta("[Codex] ") : chalk3.cyan("[Claude] ");
|
|
12305
|
+
const sessionSuffix = f.sessionId ? chalk3.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
|
|
12306
|
+
console.log(
|
|
12307
|
+
` ${ts}${proj}${agentBadge}` + chalk3.yellow(f.toolName) + chalk3.dim(` \xD7${f.count} `) + chalk3.gray(f.commandPreview) + sessionSuffix
|
|
12308
|
+
);
|
|
12309
|
+
}
|
|
12310
|
+
if (!drillDown && scan.loopFindings.length > topN) {
|
|
12004
12311
|
console.log(
|
|
12005
|
-
chalk3.dim(
|
|
12312
|
+
chalk3.dim(
|
|
12313
|
+
` \u2026 and ${scan.loopFindings.length - topN} more (--drill-down for full list)`
|
|
12314
|
+
)
|
|
12006
12315
|
);
|
|
12007
12316
|
}
|
|
12317
|
+
const stuckTools = computeStuckTools(scan.loopFindings);
|
|
12318
|
+
if (stuckTools.length > 0) {
|
|
12319
|
+
console.log("");
|
|
12320
|
+
console.log(" " + chalk3.dim("Most stuck tools:"));
|
|
12321
|
+
for (const t of stuckTools) {
|
|
12322
|
+
console.log(
|
|
12323
|
+
chalk3.dim(" ") + chalk3.yellow(t.toolName.padEnd(8)) + chalk3.dim(" ") + chalk3.dim(`\xD7${t.waste} repeats`.padEnd(14)) + chalk3.dim(` (${t.pct}%)`)
|
|
12324
|
+
);
|
|
12325
|
+
}
|
|
12326
|
+
}
|
|
12327
|
+
console.log("");
|
|
12328
|
+
}
|
|
12329
|
+
for (const section of summary.sections) {
|
|
12330
|
+
const reviewRules = section.rules.filter((r) => r.verdict !== "block");
|
|
12331
|
+
if (reviewRules.length === 0) continue;
|
|
12332
|
+
const enableHint = section.shieldKey ? chalk3.dim(` \u2192 node9 shield enable ${section.shieldKey}`) : "";
|
|
12333
|
+
console.log(" " + chalk3.dim("\u2500".repeat(70)));
|
|
12334
|
+
console.log(
|
|
12335
|
+
" " + chalk3.bold(section.label) + (section.subtitle ? chalk3.dim(` \xB7 ${section.subtitle}`) : "") + " " + chalk3.yellow(`${section.reviewCount} review`) + enableHint
|
|
12336
|
+
);
|
|
12337
|
+
for (const rule of reviewRules) {
|
|
12338
|
+
printRuleGroup(rule, topN, drillDown, previewWidth);
|
|
12339
|
+
}
|
|
12340
|
+
console.log("");
|
|
12341
|
+
}
|
|
12342
|
+
const activeShieldIds = new Set(
|
|
12343
|
+
summary.sections.filter((s) => s.sourceType === "shield" && s.shieldKey).map((s) => s.shieldKey)
|
|
12344
|
+
);
|
|
12345
|
+
const emptyShields = Object.keys(SHIELDS).filter((n) => !activeShieldIds.has(n)).sort();
|
|
12346
|
+
if (emptyShields.length > 0) {
|
|
12347
|
+
console.log(" " + chalk3.dim("\u2500".repeat(70)));
|
|
12348
|
+
console.log(
|
|
12349
|
+
" " + chalk3.bold("\u{1F6E1} Inactive Shields") + chalk3.dim(" \xB7 enable for more coverage")
|
|
12350
|
+
);
|
|
12351
|
+
console.log(" " + chalk3.dim(emptyShields.join(" \xB7 ")));
|
|
12352
|
+
console.log(" " + chalk3.dim("\u2192 node9 shield enable <name> to activate"));
|
|
12353
|
+
console.log("");
|
|
12008
12354
|
}
|
|
12009
|
-
console.log("");
|
|
12010
12355
|
}
|
|
12011
|
-
|
|
12012
|
-
const reviewRules = section.rules.filter((r) => r.verdict !== "block");
|
|
12013
|
-
if (reviewRules.length === 0) continue;
|
|
12014
|
-
const enableHint = section.shieldKey ? chalk3.dim(` \u2192 node9 shield enable ${section.shieldKey}`) : "";
|
|
12356
|
+
if (blast.reachable.length > 0 || blast.envFindings.length > 0) {
|
|
12015
12357
|
console.log(" " + chalk3.dim("\u2500".repeat(70)));
|
|
12016
12358
|
console.log(
|
|
12017
|
-
" " + chalk3.bold(
|
|
12359
|
+
" " + chalk3.bold("\u{1F52D} Blast Radius") + chalk3.dim(
|
|
12360
|
+
` \xB7 ${blastExposures} exposure${blastExposures !== 1 ? "s" : ""} an AI agent can reach right now`
|
|
12361
|
+
)
|
|
12018
12362
|
);
|
|
12019
|
-
|
|
12020
|
-
|
|
12363
|
+
console.log("");
|
|
12364
|
+
if (blast.reachable.length > 0) {
|
|
12365
|
+
for (const p of blast.reachable) {
|
|
12366
|
+
console.log(
|
|
12367
|
+
" " + chalk3.red("\u2717 ") + chalk3.yellow(p.label.padEnd(38)) + chalk3.dim(p.description)
|
|
12368
|
+
);
|
|
12369
|
+
}
|
|
12370
|
+
}
|
|
12371
|
+
if (blast.envFindings.length > 0) {
|
|
12372
|
+
for (const f of blast.envFindings) {
|
|
12373
|
+
console.log(
|
|
12374
|
+
" " + chalk3.red("\u2717 ") + chalk3.yellow(f.key.padEnd(38)) + chalk3.dim(f.patternName + " in environment")
|
|
12375
|
+
);
|
|
12376
|
+
}
|
|
12021
12377
|
}
|
|
12022
12378
|
console.log("");
|
|
12023
|
-
}
|
|
12024
|
-
const activeShieldIds = new Set(
|
|
12025
|
-
summary.sections.filter((s) => s.sourceType === "shield" && s.shieldKey).map((s) => s.shieldKey)
|
|
12026
|
-
);
|
|
12027
|
-
const emptyShields = Object.keys(SHIELDS).filter((n) => !activeShieldIds.has(n)).sort();
|
|
12028
|
-
if (emptyShields.length > 0) {
|
|
12029
|
-
console.log(" " + chalk3.dim("\u2500".repeat(70)));
|
|
12030
12379
|
console.log(
|
|
12031
|
-
|
|
12380
|
+
chalk3.dim(
|
|
12381
|
+
" \u2192 Run `node9 shield enable project-jail` to block agent access to these files."
|
|
12382
|
+
)
|
|
12032
12383
|
);
|
|
12033
|
-
console.log(" " + chalk3.dim(emptyShields.join(" \xB7 ")));
|
|
12034
|
-
console.log(" " + chalk3.dim("\u2192 node9 shield enable <name> to activate"));
|
|
12035
12384
|
console.log("");
|
|
12036
12385
|
}
|
|
12037
|
-
|
|
12038
|
-
|
|
12039
|
-
|
|
12040
|
-
|
|
12041
|
-
|
|
12042
|
-
|
|
12043
|
-
);
|
|
12044
|
-
console.log("");
|
|
12045
|
-
if (blast.reachable.length > 0) {
|
|
12046
|
-
for (const p of blast.reachable) {
|
|
12386
|
+
if (isWired) {
|
|
12387
|
+
console.log(chalk3.green(" \u2705 node9 is active \u2014 your future sessions are protected."));
|
|
12388
|
+
console.log(
|
|
12389
|
+
chalk3.dim(" Run ") + chalk3.cyan("node9 report") + chalk3.dim(" to see live protection stats.")
|
|
12390
|
+
);
|
|
12391
|
+
if (drillDown) {
|
|
12047
12392
|
console.log(
|
|
12048
|
-
|
|
12393
|
+
chalk3.dim(" Run ") + chalk3.cyan("node9 sessions --detail <session-id>") + chalk3.dim(" to see the full conversation for any session above.")
|
|
12049
12394
|
);
|
|
12050
|
-
}
|
|
12051
|
-
}
|
|
12052
|
-
if (blast.envFindings.length > 0) {
|
|
12053
|
-
for (const f of blast.envFindings) {
|
|
12395
|
+
} else {
|
|
12054
12396
|
console.log(
|
|
12055
|
-
|
|
12397
|
+
chalk3.dim(" Run ") + chalk3.cyan("node9 scan --drill-down") + chalk3.dim(" to see full commands and session IDs.")
|
|
12056
12398
|
);
|
|
12057
12399
|
}
|
|
12058
|
-
}
|
|
12059
|
-
console.log("");
|
|
12060
|
-
console.log(
|
|
12061
|
-
" Security Score: " + scoreLabel(blast.score) + chalk3.dim(
|
|
12062
|
-
` (${blast.reachable.length + blast.envFindings.length} exposure${blast.reachable.length + blast.envFindings.length !== 1 ? "s" : ""})`
|
|
12063
|
-
)
|
|
12064
|
-
);
|
|
12065
|
-
console.log(
|
|
12066
|
-
chalk3.dim(
|
|
12067
|
-
"\n Run `node9 shield enable project-jail` to block agent access to these files."
|
|
12068
|
-
)
|
|
12069
|
-
);
|
|
12070
|
-
console.log("");
|
|
12071
|
-
}
|
|
12072
|
-
if (isWired) {
|
|
12073
|
-
console.log(chalk3.green(" \u2705 node9 is active \u2014 your future sessions are protected."));
|
|
12074
|
-
console.log(
|
|
12075
|
-
chalk3.dim(" Run ") + chalk3.cyan("node9 report") + chalk3.dim(" to see live protection stats.")
|
|
12076
|
-
);
|
|
12077
|
-
if (drillDown) {
|
|
12078
|
-
console.log(
|
|
12079
|
-
chalk3.dim(" Run ") + chalk3.cyan("node9 sessions --detail <session-id>") + chalk3.dim(" to see the full conversation for any session above.")
|
|
12080
|
-
);
|
|
12081
12400
|
} else {
|
|
12401
|
+
const riskySummary = totalFindings + scan.dlpFindings.length;
|
|
12402
|
+
if (riskySummary > 0) {
|
|
12403
|
+
console.log(
|
|
12404
|
+
chalk3.yellow.bold(
|
|
12405
|
+
` \u26A1 ${riskySummary} operation${riskySummary !== 1 ? "s" : ""} ran unprotected.`
|
|
12406
|
+
) + chalk3.dim(" node9 would have caught them.")
|
|
12407
|
+
);
|
|
12408
|
+
console.log("");
|
|
12409
|
+
}
|
|
12410
|
+
console.log(chalk3.bold(" Enable real-time protection:"));
|
|
12411
|
+
console.log("");
|
|
12082
12412
|
console.log(
|
|
12083
|
-
|
|
12413
|
+
" " + chalk3.cyan("npm install -g @node9/proxy") + chalk3.dim(" && ") + chalk3.cyan("node9 init --recommended")
|
|
12084
12414
|
);
|
|
12085
|
-
|
|
12086
|
-
} else {
|
|
12087
|
-
const riskySummary = totalFindings + scan.dlpFindings.length;
|
|
12088
|
-
if (riskySummary > 0) {
|
|
12415
|
+
console.log("");
|
|
12089
12416
|
console.log(
|
|
12090
|
-
chalk3.
|
|
12091
|
-
|
|
12092
|
-
)
|
|
12417
|
+
chalk3.dim(
|
|
12418
|
+
" Hooks into Claude Code automatically. Every tool call checked before it runs."
|
|
12419
|
+
)
|
|
12093
12420
|
);
|
|
12421
|
+
console.log(" " + chalk3.dim("\u2192 ") + chalk3.underline("https://node9.ai"));
|
|
12094
12422
|
}
|
|
12095
12423
|
console.log("");
|
|
12096
|
-
|
|
12097
|
-
|
|
12098
|
-
|
|
12099
|
-
|
|
12100
|
-
|
|
12101
|
-
|
|
12102
|
-
|
|
12103
|
-
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
|
|
12116
|
-
|
|
12117
|
-
|
|
12118
|
-
|
|
12119
|
-
{ id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
|
|
12120
|
-
]);
|
|
12121
|
-
await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/scan/push`, {
|
|
12122
|
-
method: "POST",
|
|
12123
|
-
headers: {
|
|
12124
|
-
"Content-Type": "application/json",
|
|
12125
|
-
"x-node9-internal": internalToken
|
|
12126
|
-
},
|
|
12127
|
-
body: JSON.stringify({ status: "complete", summary: pushSummary }),
|
|
12128
|
-
signal: AbortSignal.timeout(3e3)
|
|
12129
|
-
});
|
|
12130
|
-
openBrowserLocal();
|
|
12131
|
-
} catch {
|
|
12424
|
+
if (!isTestingMode()) {
|
|
12425
|
+
if (isWired) {
|
|
12426
|
+
const url = `http://${DAEMON_HOST}:${DAEMON_PORT}/?openscan=1`;
|
|
12427
|
+
if (isDaemonRunning()) {
|
|
12428
|
+
const internalToken = getInternalToken();
|
|
12429
|
+
if (internalToken) {
|
|
12430
|
+
try {
|
|
12431
|
+
const pushSummary = buildScanSummary([
|
|
12432
|
+
{ id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
|
|
12433
|
+
{ id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
|
|
12434
|
+
{ id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
|
|
12435
|
+
]);
|
|
12436
|
+
await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/scan/push`, {
|
|
12437
|
+
method: "POST",
|
|
12438
|
+
headers: {
|
|
12439
|
+
"Content-Type": "application/json",
|
|
12440
|
+
"x-node9-internal": internalToken
|
|
12441
|
+
},
|
|
12442
|
+
body: JSON.stringify({ status: "complete", summary: pushSummary }),
|
|
12443
|
+
signal: AbortSignal.timeout(3e3)
|
|
12444
|
+
});
|
|
12445
|
+
} catch {
|
|
12446
|
+
}
|
|
12132
12447
|
}
|
|
12133
12448
|
}
|
|
12449
|
+
if (isDaemonRunning()) {
|
|
12450
|
+
console.log(" " + chalk3.cyan("\u{1F310} View in browser:") + " " + chalk3.underline(url));
|
|
12451
|
+
} else {
|
|
12452
|
+
console.log(
|
|
12453
|
+
" " + chalk3.dim("\u{1F4CA} To view in browser, start the daemon: ") + chalk3.cyan("node9 daemon --background")
|
|
12454
|
+
);
|
|
12455
|
+
}
|
|
12456
|
+
console.log("");
|
|
12134
12457
|
}
|
|
12135
|
-
if (isDaemonRunning()) {
|
|
12136
|
-
console.log(" " + chalk3.cyan("\u{1F310} View in browser:") + " " + chalk3.underline(url));
|
|
12137
|
-
} else {
|
|
12138
|
-
console.log(
|
|
12139
|
-
" " + chalk3.dim("\u{1F4CA} To view in browser, start the daemon: ") + chalk3.cyan("node9 daemon --background")
|
|
12140
|
-
);
|
|
12141
|
-
}
|
|
12142
|
-
console.log("");
|
|
12143
12458
|
}
|
|
12144
12459
|
}
|
|
12145
|
-
|
|
12460
|
+
);
|
|
12146
12461
|
}
|
|
12147
12462
|
var CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, TERMINAL_ESCAPE_RE, LOOP_TOOLS, LOOP_THRESHOLD, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, DEFAULT_RULE_NAMES;
|
|
12148
12463
|
var init_scan = __esm({
|
|
@@ -12509,7 +12824,6 @@ import net2 from "net";
|
|
|
12509
12824
|
import fs17 from "fs";
|
|
12510
12825
|
import path19 from "path";
|
|
12511
12826
|
import os14 from "os";
|
|
12512
|
-
import { spawn as spawn3 } from "child_process";
|
|
12513
12827
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
12514
12828
|
function loadInsightCounts() {
|
|
12515
12829
|
try {
|
|
@@ -12680,14 +12994,6 @@ function readBody(req) {
|
|
|
12680
12994
|
req.on("end", () => resolve(body));
|
|
12681
12995
|
});
|
|
12682
12996
|
}
|
|
12683
|
-
function openBrowser(url) {
|
|
12684
|
-
if (process.env.NODE9_TESTING === "1") return;
|
|
12685
|
-
try {
|
|
12686
|
-
const args = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
|
|
12687
|
-
spawn3(args[0], args.slice(1), { detached: true, stdio: "ignore" }).unref();
|
|
12688
|
-
} catch {
|
|
12689
|
-
}
|
|
12690
|
-
}
|
|
12691
12997
|
function estimateToolCost(tool, args) {
|
|
12692
12998
|
const a = args ?? {};
|
|
12693
12999
|
const t = tool.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
@@ -12800,6 +13106,18 @@ function startActivitySocket() {
|
|
|
12800
13106
|
});
|
|
12801
13107
|
return;
|
|
12802
13108
|
}
|
|
13109
|
+
if (data.status === "execution-completed") {
|
|
13110
|
+
broadcast("execution-result", {
|
|
13111
|
+
id: data.id,
|
|
13112
|
+
ts: data.ts,
|
|
13113
|
+
tool: data.tool,
|
|
13114
|
+
agent: data.agent,
|
|
13115
|
+
mcpServer: data.mcpServer,
|
|
13116
|
+
durationMs: data.durationMs,
|
|
13117
|
+
isError: data.isError
|
|
13118
|
+
});
|
|
13119
|
+
return;
|
|
13120
|
+
}
|
|
12803
13121
|
if (data.status === "pending") {
|
|
12804
13122
|
broadcast("activity", {
|
|
12805
13123
|
id: data.id,
|
|
@@ -12807,7 +13125,8 @@ function startActivitySocket() {
|
|
|
12807
13125
|
tool: data.tool,
|
|
12808
13126
|
args: redactArgs(data.args),
|
|
12809
13127
|
status: "pending",
|
|
12810
|
-
agent: data.agent
|
|
13128
|
+
agent: data.agent,
|
|
13129
|
+
mcpServer: data.mcpServer
|
|
12811
13130
|
});
|
|
12812
13131
|
} else {
|
|
12813
13132
|
if (data.status === "allow") {
|
|
@@ -12834,7 +13153,9 @@ function startActivitySocket() {
|
|
|
12834
13153
|
id: data.id,
|
|
12835
13154
|
status: data.status,
|
|
12836
13155
|
label: data.label,
|
|
12837
|
-
costEstimate
|
|
13156
|
+
costEstimate,
|
|
13157
|
+
agent: data.agent,
|
|
13158
|
+
mcpServer: data.mcpServer
|
|
12838
13159
|
});
|
|
12839
13160
|
}
|
|
12840
13161
|
} catch {
|
|
@@ -13516,7 +13837,6 @@ function startDaemon() {
|
|
|
13516
13837
|
const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
|
|
13517
13838
|
const watchMode = process.env.NODE9_WATCH_MODE === "1";
|
|
13518
13839
|
let idleTimer;
|
|
13519
|
-
let browserOpened = false;
|
|
13520
13840
|
function resetIdleTimer() {
|
|
13521
13841
|
if (watchMode) return;
|
|
13522
13842
|
if (idleTimer) clearTimeout(idleTimer);
|
|
@@ -13625,12 +13945,6 @@ data: ${JSON.stringify(item.data)}
|
|
|
13625
13945
|
}
|
|
13626
13946
|
});
|
|
13627
13947
|
}
|
|
13628
|
-
if (req.method === "POST" && pathname === "/browser-opened") {
|
|
13629
|
-
if (req.headers["x-node9-internal"] !== internalToken) return res.writeHead(403).end();
|
|
13630
|
-
browserOpened = true;
|
|
13631
|
-
res.writeHead(200).end();
|
|
13632
|
-
return;
|
|
13633
|
-
}
|
|
13634
13948
|
if (req.method === "POST" && pathname === "/check") {
|
|
13635
13949
|
try {
|
|
13636
13950
|
resetIdleTimer();
|
|
@@ -13691,7 +14005,9 @@ data: ${JSON.stringify(item.data)}
|
|
|
13691
14005
|
ts: entry.timestamp,
|
|
13692
14006
|
tool: toolName,
|
|
13693
14007
|
args: redactArgs(args),
|
|
13694
|
-
status: "pending"
|
|
14008
|
+
status: "pending",
|
|
14009
|
+
agent: entry.agent,
|
|
14010
|
+
mcpServer: entry.mcpServer
|
|
13695
14011
|
});
|
|
13696
14012
|
}
|
|
13697
14013
|
const projectCwd = typeof cwd === "string" && path25.isAbsolute(cwd) ? cwd : void 0;
|
|
@@ -13714,11 +14030,6 @@ data: ${JSON.stringify(item.data)}
|
|
|
13714
14030
|
// Terminal uses this to show the 💡 insight line on the Nth consecutive approval.
|
|
13715
14031
|
allowCount: (insightCounts.get(toolName) ?? 0) + 1
|
|
13716
14032
|
});
|
|
13717
|
-
const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
|
|
13718
|
-
if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
|
|
13719
|
-
browserOpened = true;
|
|
13720
|
-
openBrowser(`http://127.0.0.1:${DAEMON_PORT}/`);
|
|
13721
|
-
}
|
|
13722
14033
|
}
|
|
13723
14034
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
13724
14035
|
res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
|
|
@@ -13740,7 +14051,9 @@ data: ${JSON.stringify(item.data)}
|
|
|
13740
14051
|
broadcast("activity-result", {
|
|
13741
14052
|
id,
|
|
13742
14053
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
13743
|
-
label: result.blockedByLabel
|
|
14054
|
+
label: result.blockedByLabel,
|
|
14055
|
+
agent: e.agent,
|
|
14056
|
+
mcpServer: e.mcpServer
|
|
13744
14057
|
});
|
|
13745
14058
|
clearTimeout(e.timer);
|
|
13746
14059
|
const decision = result.approved ? "allow" : "deny";
|
|
@@ -14912,7 +15225,7 @@ import fs41 from "fs";
|
|
|
14912
15225
|
import os37 from "os";
|
|
14913
15226
|
import path43 from "path";
|
|
14914
15227
|
import readline5 from "readline";
|
|
14915
|
-
import { spawn as
|
|
15228
|
+
import { spawn as spawn9 } from "child_process";
|
|
14916
15229
|
function getIcon(tool) {
|
|
14917
15230
|
const t = tool.toLowerCase();
|
|
14918
15231
|
for (const [k, v] of Object.entries(ICONS)) {
|
|
@@ -14997,10 +15310,13 @@ function wrappedLineCount(text) {
|
|
|
14997
15310
|
const len = visibleLength(text);
|
|
14998
15311
|
return Math.max(1, Math.ceil(len / cols));
|
|
14999
15312
|
}
|
|
15000
|
-
function agentLabel(agent) {
|
|
15001
|
-
if (!agent || agent === "Terminal")
|
|
15313
|
+
function agentLabel(agent, mcpServer) {
|
|
15314
|
+
if (!agent || agent === "Terminal") {
|
|
15315
|
+
return mcpServer ? chalk27.dim(`[\u2192 ${mcpServer}] `) : "";
|
|
15316
|
+
}
|
|
15002
15317
|
const short = agent === "Claude Code" ? "Claude" : agent === "Gemini CLI" ? "Gemini" : agent === "Unknown Agent" ? "" : agent.split(" ")[0];
|
|
15003
|
-
|
|
15318
|
+
if (!short) return mcpServer ? chalk27.dim(`[\u2192 ${mcpServer}] `) : "";
|
|
15319
|
+
return mcpServer ? chalk27.dim(`[${short} \u2192 ${mcpServer}] `) : chalk27.dim(`[${short}] `);
|
|
15004
15320
|
}
|
|
15005
15321
|
function formatBase(activity) {
|
|
15006
15322
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
@@ -15008,7 +15324,7 @@ function formatBase(activity) {
|
|
|
15008
15324
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
15009
15325
|
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os37.homedir(), "~");
|
|
15010
15326
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
15011
|
-
return `${chalk27.gray(time)} ${icon} ${agentLabel(activity.agent)}${chalk27.white.bold(toolName)} ${chalk27.dim(argsPreview)}`;
|
|
15327
|
+
return `${chalk27.gray(time)} ${icon} ${agentLabel(activity.agent, activity.mcpServer)}${chalk27.white.bold(toolName)} ${chalk27.dim(argsPreview)}`;
|
|
15012
15328
|
}
|
|
15013
15329
|
function renderResult(activity, result) {
|
|
15014
15330
|
const base = formatBase(activity);
|
|
@@ -15062,7 +15378,7 @@ async function ensureDaemon() {
|
|
|
15062
15378
|
} catch {
|
|
15063
15379
|
}
|
|
15064
15380
|
console.log(chalk27.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
15065
|
-
const child =
|
|
15381
|
+
const child = spawn9(process.execPath, [process.argv[1], "daemon"], {
|
|
15066
15382
|
detached: true,
|
|
15067
15383
|
stdio: "ignore",
|
|
15068
15384
|
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
@@ -15462,22 +15778,6 @@ async function startTail(options = {}) {
|
|
|
15462
15778
|
process.stdin.on("keypress", onKeypress);
|
|
15463
15779
|
}
|
|
15464
15780
|
const dashboardUrl = `http://127.0.0.1:${port}/`;
|
|
15465
|
-
try {
|
|
15466
|
-
const browserEnabled = getConfig().settings.approvers?.browser !== false;
|
|
15467
|
-
if (browserEnabled) {
|
|
15468
|
-
if (process.platform === "darwin") execSync3(`open "${dashboardUrl}"`, { stdio: "ignore" });
|
|
15469
|
-
else if (process.platform === "win32")
|
|
15470
|
-
execSync3(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
|
|
15471
|
-
else execSync3(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
|
|
15472
|
-
const intToken = getInternalToken();
|
|
15473
|
-
fetch(`http://127.0.0.1:${port}/browser-opened`, {
|
|
15474
|
-
method: "POST",
|
|
15475
|
-
headers: intToken ? { "X-Node9-Internal": intToken } : {}
|
|
15476
|
-
}).catch(() => {
|
|
15477
|
-
});
|
|
15478
|
-
}
|
|
15479
|
-
} catch {
|
|
15480
|
-
}
|
|
15481
15781
|
const auditLog = path43.join(os37.homedir(), ".node9", "audit.log");
|
|
15482
15782
|
try {
|
|
15483
15783
|
const unackedDlp = fs41.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
|
|
@@ -15657,6 +15957,17 @@ async function startTail(options = {}) {
|
|
|
15657
15957
|
orphanedResults.set(data.id, data);
|
|
15658
15958
|
}
|
|
15659
15959
|
}
|
|
15960
|
+
if (event === "execution-result") {
|
|
15961
|
+
const exec = data;
|
|
15962
|
+
const time = new Date(Date.now()).toLocaleTimeString([], { hour12: false });
|
|
15963
|
+
const arrow = exec.isError ? chalk27.red(" \u21B3 \u2717") : chalk27.green(" \u21B3 \u2713");
|
|
15964
|
+
const label = agentLabel(exec.agent, exec.mcpServer);
|
|
15965
|
+
const tool = (exec.tool ?? "").slice(0, 16);
|
|
15966
|
+
const duration = typeof exec.durationMs === "number" ? chalk27.dim(` (${exec.durationMs}ms)`) : "";
|
|
15967
|
+
console.log(
|
|
15968
|
+
`${chalk27.gray(time)} ${arrow} ${label}${chalk27.dim(tool)}${chalk27.dim(" completed")}${duration}`
|
|
15969
|
+
);
|
|
15970
|
+
}
|
|
15660
15971
|
}
|
|
15661
15972
|
req.on("error", (err2) => {
|
|
15662
15973
|
const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
|
|
@@ -15670,8 +15981,6 @@ var init_tail = __esm({
|
|
|
15670
15981
|
"src/tui/tail.ts"() {
|
|
15671
15982
|
"use strict";
|
|
15672
15983
|
init_daemon2();
|
|
15673
|
-
init_daemon();
|
|
15674
|
-
init_core();
|
|
15675
15984
|
PID_FILE = path43.join(os37.homedir(), ".node9", "daemon.pid");
|
|
15676
15985
|
ICONS = {
|
|
15677
15986
|
bash: "\u{1F4BB}",
|
|
@@ -16126,7 +16435,7 @@ function parseDuration(str) {
|
|
|
16126
16435
|
init_orchestrator();
|
|
16127
16436
|
import readline from "readline";
|
|
16128
16437
|
import chalk6 from "chalk";
|
|
16129
|
-
import { spawn as
|
|
16438
|
+
import { spawn as spawn3 } from "child_process";
|
|
16130
16439
|
import { execa } from "execa";
|
|
16131
16440
|
import { parseCommandString } from "execa";
|
|
16132
16441
|
|
|
@@ -16213,11 +16522,11 @@ async function runProxy(targetCommand) {
|
|
|
16213
16522
|
}
|
|
16214
16523
|
console.error(chalk6.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
|
|
16215
16524
|
const spawnEnv = { ...process.env, FORCE_COLOR: "1" };
|
|
16216
|
-
const child = useShell ?
|
|
16525
|
+
const child = useShell ? spawn3("/bin/bash", ["-c", targetCommand], {
|
|
16217
16526
|
stdio: ["pipe", "pipe", "inherit"],
|
|
16218
16527
|
shell: false,
|
|
16219
16528
|
env: spawnEnv
|
|
16220
|
-
}) :
|
|
16529
|
+
}) : spawn3(executable, args, { stdio: ["pipe", "pipe", "inherit"], shell: false, env: spawnEnv });
|
|
16221
16530
|
const agentIn = readline.createInterface({ input: process.stdin, terminal: false });
|
|
16222
16531
|
agentIn.on("line", async (line) => {
|
|
16223
16532
|
let message;
|
|
@@ -16290,12 +16599,12 @@ init_config();
|
|
|
16290
16599
|
init_policy();
|
|
16291
16600
|
import chalk7 from "chalk";
|
|
16292
16601
|
import fs28 from "fs";
|
|
16293
|
-
import { spawn as
|
|
16602
|
+
import { spawn as spawn5 } from "child_process";
|
|
16294
16603
|
import path29 from "path";
|
|
16295
16604
|
import os24 from "os";
|
|
16296
16605
|
|
|
16297
16606
|
// src/undo.ts
|
|
16298
|
-
import { spawnSync as spawnSync5, spawn as
|
|
16607
|
+
import { spawnSync as spawnSync5, spawn as spawn4 } from "child_process";
|
|
16299
16608
|
import crypto3 from "crypto";
|
|
16300
16609
|
import fs26 from "fs";
|
|
16301
16610
|
import net3 from "net";
|
|
@@ -16547,7 +16856,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
16547
16856
|
notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
|
|
16548
16857
|
fs26.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
16549
16858
|
if (shouldGc) {
|
|
16550
|
-
|
|
16859
|
+
spawn4("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
16551
16860
|
}
|
|
16552
16861
|
return commitHash;
|
|
16553
16862
|
} catch (err2) {
|
|
@@ -16876,7 +17185,7 @@ RAW: ${raw}
|
|
|
16876
17185
|
]) {
|
|
16877
17186
|
delete safeEnv[key];
|
|
16878
17187
|
}
|
|
16879
|
-
const d =
|
|
17188
|
+
const d = spawn5(process.execPath, [scriptPath, "daemon"], {
|
|
16880
17189
|
detached: true,
|
|
16881
17190
|
stdio: "ignore",
|
|
16882
17191
|
env: { ...safeEnv, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
|
|
@@ -18638,7 +18947,7 @@ init_daemon2();
|
|
|
18638
18947
|
init_daemon();
|
|
18639
18948
|
init_daemon_starter();
|
|
18640
18949
|
import chalk12 from "chalk";
|
|
18641
|
-
import { spawn as
|
|
18950
|
+
import { spawn as spawn6 } from "child_process";
|
|
18642
18951
|
var VALID_ACTIONS = "start | stop | restart | status | install | uninstall";
|
|
18643
18952
|
function registerDaemonCommand(program2) {
|
|
18644
18953
|
program2.command("daemon").description("Manage the local approval daemon").argument("[action]", `${VALID_ACTIONS} (default: start)`).option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").option(
|
|
@@ -18676,7 +18985,7 @@ function registerDaemonCommand(program2) {
|
|
|
18676
18985
|
if (cmd === "restart") {
|
|
18677
18986
|
stopDaemon();
|
|
18678
18987
|
await new Promise((r) => setTimeout(r, 500));
|
|
18679
|
-
const child =
|
|
18988
|
+
const child = spawn6(process.execPath, [process.argv[1], "daemon"], {
|
|
18680
18989
|
detached: true,
|
|
18681
18990
|
stdio: "ignore",
|
|
18682
18991
|
env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
|
|
@@ -18710,7 +19019,7 @@ function registerDaemonCommand(program2) {
|
|
|
18710
19019
|
console.log(chalk12.green(`\u{1F310} Opened browser: http://${DAEMON_HOST}:${DAEMON_PORT}/`));
|
|
18711
19020
|
process.exit(0);
|
|
18712
19021
|
}
|
|
18713
|
-
const child =
|
|
19022
|
+
const child = spawn6(process.execPath, [process.argv[1], "daemon"], {
|
|
18714
19023
|
detached: true,
|
|
18715
19024
|
stdio: "ignore"
|
|
18716
19025
|
});
|
|
@@ -18725,7 +19034,7 @@ function registerDaemonCommand(program2) {
|
|
|
18725
19034
|
process.exit(0);
|
|
18726
19035
|
}
|
|
18727
19036
|
if (options.background) {
|
|
18728
|
-
const child =
|
|
19037
|
+
const child = spawn6(process.execPath, [process.argv[1], "daemon"], {
|
|
18729
19038
|
detached: true,
|
|
18730
19039
|
stdio: "ignore"
|
|
18731
19040
|
});
|
|
@@ -18933,14 +19242,18 @@ function fireTelemetryPing(agents) {
|
|
|
18933
19242
|
}
|
|
18934
19243
|
}
|
|
18935
19244
|
function registerInitCommand(program2) {
|
|
18936
|
-
program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option(
|
|
19245
|
+
program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option(
|
|
19246
|
+
"-m, --mode <mode>",
|
|
19247
|
+
"Initial security mode: standard | strict | audit | observe (logs would-block, never blocks)",
|
|
19248
|
+
"standard"
|
|
19249
|
+
).option("--skip-setup", "Only create config \u2014 do not wire AI agents").option(
|
|
18937
19250
|
"--recommended",
|
|
18938
19251
|
"Non-interactive: enable bash-safe + filesystem + project-jail shields without prompting"
|
|
18939
19252
|
).action(
|
|
18940
19253
|
async (options) => {
|
|
18941
19254
|
console.log(chalk14.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
18942
19255
|
let chosenMode = options.mode.toLowerCase();
|
|
18943
|
-
if (!["standard", "strict", "audit"].includes(chosenMode)) {
|
|
19256
|
+
if (!["standard", "strict", "audit", "observe"].includes(chosenMode)) {
|
|
18944
19257
|
chosenMode = DEFAULT_CONFIG.settings.mode;
|
|
18945
19258
|
}
|
|
18946
19259
|
{
|
|
@@ -19382,7 +19695,7 @@ function registerUndoCommand(program2) {
|
|
|
19382
19695
|
// src/cli/commands/watch.ts
|
|
19383
19696
|
init_daemon();
|
|
19384
19697
|
import chalk17 from "chalk";
|
|
19385
|
-
import { spawn as
|
|
19698
|
+
import { spawn as spawn7, spawnSync as spawnSync6 } from "child_process";
|
|
19386
19699
|
function registerWatchCommand(program2) {
|
|
19387
19700
|
program2.command("watch").description("Run a command under Node9 watch mode (daemon stays alive for the session)").argument("<command>", "Command to run").argument("[args...]", "Arguments for the command").action(async (cmd, args) => {
|
|
19388
19701
|
let port = DAEMON_PORT;
|
|
@@ -19398,7 +19711,7 @@ function registerWatchCommand(program2) {
|
|
|
19398
19711
|
}
|
|
19399
19712
|
} catch {
|
|
19400
19713
|
console.error(chalk17.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
|
|
19401
|
-
const child =
|
|
19714
|
+
const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
|
|
19402
19715
|
detached: true,
|
|
19403
19716
|
stdio: "ignore",
|
|
19404
19717
|
env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_WATCH_MODE: "1" }
|
|
@@ -19444,7 +19757,7 @@ function registerWatchCommand(program2) {
|
|
|
19444
19757
|
init_orchestrator();
|
|
19445
19758
|
import readline3 from "readline";
|
|
19446
19759
|
import chalk18 from "chalk";
|
|
19447
|
-
import { spawn as
|
|
19760
|
+
import { spawn as spawn8 } from "child_process";
|
|
19448
19761
|
import { execa as execa2 } from "execa";
|
|
19449
19762
|
init_provenance();
|
|
19450
19763
|
|
|
@@ -19565,6 +19878,18 @@ function extractMcpServer(toolName) {
|
|
|
19565
19878
|
const match = toolName.match(/^mcp__([^_](?:[^_]|_(?!_))*?)__/i);
|
|
19566
19879
|
return match?.[1];
|
|
19567
19880
|
}
|
|
19881
|
+
function normalizeClientName(name) {
|
|
19882
|
+
if (typeof name !== "string" || name.length === 0) return void 0;
|
|
19883
|
+
const lower = name.toLowerCase();
|
|
19884
|
+
if (lower.includes("claude")) return "Claude";
|
|
19885
|
+
if (lower.includes("cursor")) return "Cursor";
|
|
19886
|
+
if (lower.includes("codex")) return "Codex";
|
|
19887
|
+
if (lower.includes("gemini")) return "Gemini";
|
|
19888
|
+
if (lower.includes("cline")) return "Cline";
|
|
19889
|
+
if (lower.includes("continue")) return "Continue";
|
|
19890
|
+
const sanitized = sanitize4(name).slice(0, 40);
|
|
19891
|
+
return sanitized.length > 0 ? sanitized : void 0;
|
|
19892
|
+
}
|
|
19568
19893
|
function tokenize4(cmd) {
|
|
19569
19894
|
const tokens = [];
|
|
19570
19895
|
let current = "";
|
|
@@ -19637,7 +19962,7 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
19637
19962
|
const safeEnv = Object.fromEntries(
|
|
19638
19963
|
Object.entries(process.env).filter(([k]) => !UPSTREAM_INJECTOR_VARS.has(k))
|
|
19639
19964
|
);
|
|
19640
|
-
const child =
|
|
19965
|
+
const child = spawn8(executable, cmdArgs, {
|
|
19641
19966
|
stdio: ["pipe", "pipe", "inherit"],
|
|
19642
19967
|
// control stdin/stdout; inherit stderr
|
|
19643
19968
|
shell: false,
|
|
@@ -19651,6 +19976,8 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
19651
19976
|
let pinState = "pending";
|
|
19652
19977
|
const pendingToolCalls = [];
|
|
19653
19978
|
const pendingCallNames = /* @__PURE__ */ new Map();
|
|
19979
|
+
const pendingExecutions = /* @__PURE__ */ new Map();
|
|
19980
|
+
let clientName;
|
|
19654
19981
|
const agentIn = readline3.createInterface({ input: process.stdin, terminal: false });
|
|
19655
19982
|
agentIn.on("line", async (line) => {
|
|
19656
19983
|
let message;
|
|
@@ -19673,6 +20000,13 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
19673
20000
|
child.stdin.write(line + "\n");
|
|
19674
20001
|
return;
|
|
19675
20002
|
}
|
|
20003
|
+
if (message.method === "initialize") {
|
|
20004
|
+
clientName = normalizeClientName(
|
|
20005
|
+
message.params?.clientInfo?.name
|
|
20006
|
+
);
|
|
20007
|
+
child.stdin.write(line + "\n");
|
|
20008
|
+
return;
|
|
20009
|
+
}
|
|
19676
20010
|
if (message.method === "tools/list" && message.id !== void 0 && message.id !== null) {
|
|
19677
20011
|
pendingToolsListIds.add(message.id);
|
|
19678
20012
|
}
|
|
@@ -19718,7 +20052,7 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
19718
20052
|
const toolArgs = message.params?.arguments ?? message.params?.tool_input ?? {};
|
|
19719
20053
|
const mcpServer = extractMcpServer(toolName);
|
|
19720
20054
|
const result = await authorizeHeadless(toolName, toolArgs, {
|
|
19721
|
-
agent: "MCP-Gateway",
|
|
20055
|
+
agent: clientName ?? "MCP-Gateway",
|
|
19722
20056
|
mcpServer
|
|
19723
20057
|
});
|
|
19724
20058
|
if (!result.approved) {
|
|
@@ -19748,6 +20082,12 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
19748
20082
|
}
|
|
19749
20083
|
if (message.id !== void 0 && message.id !== null) {
|
|
19750
20084
|
pendingCallNames.set(message.id, toolName);
|
|
20085
|
+
pendingExecutions.set(message.id, {
|
|
20086
|
+
ts: Date.now(),
|
|
20087
|
+
toolName,
|
|
20088
|
+
agent: clientName,
|
|
20089
|
+
mcpServer
|
|
20090
|
+
});
|
|
19751
20091
|
}
|
|
19752
20092
|
child.stdin.write(line + "\n");
|
|
19753
20093
|
} catch {
|
|
@@ -19889,6 +20229,26 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
19889
20229
|
return;
|
|
19890
20230
|
}
|
|
19891
20231
|
}
|
|
20232
|
+
const respId = parsed?.id;
|
|
20233
|
+
if (respId !== void 0 && respId !== null) {
|
|
20234
|
+
const exec = pendingExecutions.get(respId);
|
|
20235
|
+
if (exec) {
|
|
20236
|
+
pendingExecutions.delete(respId);
|
|
20237
|
+
const durationMs = Date.now() - exec.ts;
|
|
20238
|
+
const isError = parsed?.error !== void 0;
|
|
20239
|
+
notifyActivitySocket({
|
|
20240
|
+
id: String(respId),
|
|
20241
|
+
ts: Date.now(),
|
|
20242
|
+
tool: exec.toolName,
|
|
20243
|
+
status: "execution-completed",
|
|
20244
|
+
durationMs,
|
|
20245
|
+
agent: exec.agent,
|
|
20246
|
+
mcpServer: exec.mcpServer,
|
|
20247
|
+
isError
|
|
20248
|
+
}).catch(() => {
|
|
20249
|
+
});
|
|
20250
|
+
}
|
|
20251
|
+
}
|
|
19892
20252
|
const LARGE_RESPONSE_THRESHOLD = 5e5;
|
|
19893
20253
|
if (parsed?.result && line.length > LARGE_RESPONSE_THRESHOLD) {
|
|
19894
20254
|
const callId = parsed.id;
|