infracensus-collector 1.4.1 → 1.4.3
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/dist/index.js +84 -16
- package/dist/index.js.map +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14604,6 +14604,8 @@ function renderPage() {
|
|
|
14604
14604
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
14605
14605
|
<meta name="referrer" content="no-referrer">
|
|
14606
14606
|
<title>InfraCensus Collector</title>
|
|
14607
|
+
<!-- An empty icon, declared so the browser never asks for /favicon.ico. -->
|
|
14608
|
+
<link rel="icon" href="data:,">
|
|
14607
14609
|
<style>
|
|
14608
14610
|
${tokenCss()}
|
|
14609
14611
|
|
|
@@ -14786,6 +14788,16 @@ ${tokenCss()}
|
|
|
14786
14788
|
}
|
|
14787
14789
|
.bldpill .count { color: var(--muted); font-size: 0.6875rem; }
|
|
14788
14790
|
|
|
14791
|
+
/* A textarea cannot be type=password, and a list of logins pasted into one
|
|
14792
|
+
is every device password in the estate sitting in plain text on a screen
|
|
14793
|
+
somebody else can walk past. The -webkit-text-security property is the only thing
|
|
14794
|
+
that masks a multi-line field; it is supported in Chromium, Edge, Safari
|
|
14795
|
+
and Firefox 119+, and the unprefixed property is written beside it for
|
|
14796
|
+
whatever standardises. Where neither is honoured the field simply reads
|
|
14797
|
+
as it always did \u2014 so the toggle says SHOW/HIDE rather than promising
|
|
14798
|
+
secrecy the browser might not deliver. */
|
|
14799
|
+
textarea.masked { -webkit-text-security: disc; text-security: disc; }
|
|
14800
|
+
|
|
14789
14801
|
.ok { color: var(--ok); } .bad { color: var(--bad); } .dim { color: var(--muted); }
|
|
14790
14802
|
.warntext { color: var(--warn); }
|
|
14791
14803
|
|
|
@@ -14933,7 +14945,8 @@ ${tokenCss()}
|
|
|
14933
14945
|
</head>
|
|
14934
14946
|
<body>
|
|
14935
14947
|
<main>
|
|
14936
|
-
<!-- Login: shown whenever /ui/api/
|
|
14948
|
+
<!-- Login: shown whenever /ui/api/session says signed out, or a route behind
|
|
14949
|
+
the session answers 401 because it lapsed. The only credential
|
|
14937
14950
|
fields here that belong to a PERSON \u2014 they are a cloud account of this
|
|
14938
14951
|
collector's tenant, verified by the control plane, and nothing typed here
|
|
14939
14952
|
is stored. Device credentials are a different thing entirely and live on
|
|
@@ -15919,14 +15932,32 @@ ${tokenCss()}
|
|
|
15919
15932
|
if (b && b.getAttribute && b.getAttribute("id") === "refreshModules") refreshEntitlement(b);
|
|
15920
15933
|
});
|
|
15921
15934
|
|
|
15935
|
+
// Signed in or not, asked BEFORE anything behind the session. A signed-out
|
|
15936
|
+
// page used to learn it from a 401 on /ui/api/status (and on credentials and
|
|
15937
|
+
// sites right after), which the browser logs as a failed request on every
|
|
15938
|
+
// load and every poll. null means the daemon did not answer at all.
|
|
15939
|
+
async function signedIn() {
|
|
15940
|
+
try {
|
|
15941
|
+
const res = await fetch("/ui/api/session", { cache: "no-store" });
|
|
15942
|
+
if (!res.ok) return null;
|
|
15943
|
+
return (await res.json()).signedIn === true;
|
|
15944
|
+
} catch (e) { return null; }
|
|
15945
|
+
}
|
|
15946
|
+
|
|
15947
|
+
// Resolves true when the app view is showing live status and false otherwise,
|
|
15948
|
+
// so the startup chain can skip the two directories on a signed-out load.
|
|
15922
15949
|
async function refresh() {
|
|
15950
|
+
const live = await signedIn();
|
|
15951
|
+
if (live === false) { showLogin(); return false; }
|
|
15952
|
+
if (live === null) return false; // daemon restarting \u2014 the next tick retries
|
|
15923
15953
|
try {
|
|
15924
15954
|
const res = await fetch("/ui/api/status", { cache: "no-store" });
|
|
15925
|
-
if (res.status === 401) { showLogin(); return; }
|
|
15926
|
-
if (!res.ok) return; // transient \u2014 keep the last good view
|
|
15955
|
+
if (res.status === 401) { showLogin(); return false; } // lapsed between the two asks
|
|
15956
|
+
if (!res.ok) return false; // transient \u2014 keep the last good view
|
|
15927
15957
|
render(await res.json());
|
|
15928
15958
|
showApp();
|
|
15929
|
-
|
|
15959
|
+
return true;
|
|
15960
|
+
} catch (e) { return false; /* server restarting \u2014 the next tick retries */ }
|
|
15930
15961
|
}
|
|
15931
15962
|
|
|
15932
15963
|
function schedulePoll() {
|
|
@@ -16601,11 +16632,24 @@ ${tokenCss()}
|
|
|
16601
16632
|
const pasteSummary = el("summary", { style: "cursor:pointer",
|
|
16602
16633
|
text: "Paste several logins at once" });
|
|
16603
16634
|
pasteWrap.appendChild(pasteSummary);
|
|
16604
|
-
const pasteBox = el("textarea", {
|
|
16605
|
-
|
|
16635
|
+
const pasteBox = el("textarea", {
|
|
16636
|
+
id: prefix + "SshPaste",
|
|
16637
|
+
class: "masked",
|
|
16638
|
+
spellcheck: "false",
|
|
16639
|
+
rows: "4",
|
|
16640
|
+
placeholder: "admin,P@ssw0rd,EnableSecret\\nreadonly:viewpass\\nsvc-net secret123",
|
|
16641
|
+
});
|
|
16606
16642
|
pasteWrap.appendChild(field("One login per line \u2014 username, password, enable secret", pasteBox,
|
|
16607
16643
|
"Separate the fields with a comma, a colon, a tab, or spaces. The enable secret is optional. A password that itself contains a comma or colon has to go in the rows above, because a flat line cannot say which separator was part of the password."));
|
|
16608
16644
|
const pasteBtn = el("button", { type: "button", class: "subtle", text: "Add these logins" });
|
|
16645
|
+
// SHOW/HIDE, because the box is masked by default and somebody has to be
|
|
16646
|
+
// able to check what they pasted. Toggling a class rather than swapping
|
|
16647
|
+
// the element keeps the text \u2014 a re-render would lose it.
|
|
16648
|
+
const peek = el("button", { type: "button", class: "subtle", text: "Show" });
|
|
16649
|
+
peek.addEventListener("click", () => {
|
|
16650
|
+
const masked = pasteBox.classList.toggle("masked");
|
|
16651
|
+
peek.textContent = masked ? "Show" : "Hide";
|
|
16652
|
+
});
|
|
16609
16653
|
const pasteNote = el("div", { class: "hint" });
|
|
16610
16654
|
pasteBtn.addEventListener("click", () => {
|
|
16611
16655
|
const parsed = parsePastedLogins(pasteBox.value);
|
|
@@ -16617,26 +16661,39 @@ ${tokenCss()}
|
|
|
16617
16661
|
// pastes six means eight, and silently discarding the two would be the
|
|
16618
16662
|
// kind of data loss that is only noticed when a scan comes back short.
|
|
16619
16663
|
//
|
|
16620
|
-
//
|
|
16621
|
-
//
|
|
16622
|
-
//
|
|
16623
|
-
//
|
|
16624
|
-
|
|
16664
|
+
// \u2500\u2500 IDENTICAL MEANS THE WHOLE CREDENTIAL, NOT THE USERNAME \u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
16665
|
+
//
|
|
16666
|
+
// This compared USERNAMES, and that was wrong in the one way that
|
|
16667
|
+
// matters: fifteen lines reading admin/pass1 ... admin/pass15 are not
|
|
16668
|
+
// fifteen copies of one login, they are the fifteen passwords an
|
|
16669
|
+
// operator wants tried in order \u2014 the exact reason the box exists.
|
|
16670
|
+
// Fourteen of them were silently discarded.
|
|
16671
|
+
//
|
|
16672
|
+
// The rule it was reaching for is still worth keeping: the SSH loop
|
|
16673
|
+
// tries every credential against every host, so a genuinely identical
|
|
16674
|
+
// entry is a second identical login attempt per device, which is how a
|
|
16675
|
+
// lockout policy trips. Identical is username AND password AND enable.
|
|
16676
|
+
const key = (c) => JSON.stringify([c.username, c.password, c.enable || ""]);
|
|
16677
|
+
const already = sshRows.values()
|
|
16678
|
+
.filter((r) => trimmed(r.username) !== "")
|
|
16679
|
+
.map((r) => key({ username: trimmed(r.username), password: r.password, enable: r.enable }));
|
|
16625
16680
|
let added = 0;
|
|
16626
16681
|
let skipped = 0;
|
|
16627
16682
|
for (const cred of parsed) {
|
|
16628
|
-
|
|
16629
|
-
already.
|
|
16683
|
+
const k = key(cred);
|
|
16684
|
+
if (already.indexOf(k) >= 0) { skipped += 1; continue; }
|
|
16685
|
+
already.push(k);
|
|
16630
16686
|
sshRows.add(cred);
|
|
16631
16687
|
added += 1;
|
|
16632
16688
|
}
|
|
16633
16689
|
pasteBox.value = "";
|
|
16634
16690
|
pasteNote.textContent =
|
|
16635
16691
|
"Added " + added + " login" + (added === 1 ? "" : "s")
|
|
16636
|
-
+ (skipped ? ", skipped " + skipped + " already listed
|
|
16692
|
+
+ (skipped ? ", skipped " + skipped + " already listed" : "")
|
|
16637
16693
|
+ ". They are tried in the order shown, top first.";
|
|
16638
16694
|
});
|
|
16639
16695
|
pasteWrap.appendChild(pasteBtn);
|
|
16696
|
+
pasteWrap.appendChild(peek);
|
|
16640
16697
|
pasteWrap.appendChild(pasteNote);
|
|
16641
16698
|
host.appendChild(pasteWrap);
|
|
16642
16699
|
host.appendChild(el("h3", { text: "Windows logins (WMI enrichment)" }));
|
|
@@ -17446,8 +17503,10 @@ ${tokenCss()}
|
|
|
17446
17503
|
|
|
17447
17504
|
// Status first, then the two directories this page offers choices from. The
|
|
17448
17505
|
// site list lands after the status seed on purpose: the seed points the picker
|
|
17449
|
-
// at the configured id, and this only resolves it into a name.
|
|
17450
|
-
|
|
17506
|
+
// at the configured id, and this only resolves it into a name. Signed out,
|
|
17507
|
+
// neither directory is asked for: both sit behind the session and would only
|
|
17508
|
+
// answer 401. A successful login loads them itself.
|
|
17509
|
+
refresh().then((ok) => { if (ok) return loadCredentials().then(loadSites); });
|
|
17451
17510
|
schedulePoll();
|
|
17452
17511
|
})();
|
|
17453
17512
|
</script>
|
|
@@ -18455,6 +18514,15 @@ async function startUiServer(deps, port = resolveUiPort()) {
|
|
|
18455
18514
|
json(res, 200, { ok: true }, { "set-cookie": auth.clearCookie() });
|
|
18456
18515
|
return;
|
|
18457
18516
|
}
|
|
18517
|
+
if (method === "GET" && url === "/favicon.ico") {
|
|
18518
|
+
res.writeHead(204, { "cache-control": "max-age=86400" });
|
|
18519
|
+
res.end();
|
|
18520
|
+
return;
|
|
18521
|
+
}
|
|
18522
|
+
if (url === "/ui/api/session" && method === "GET") {
|
|
18523
|
+
json(res, 200, { signedIn: auth.verify(req.headers.cookie) });
|
|
18524
|
+
return;
|
|
18525
|
+
}
|
|
18458
18526
|
if (!auth.verify(req.headers.cookie)) {
|
|
18459
18527
|
json(res, 401, UNAUTHORIZED_BODY);
|
|
18460
18528
|
return;
|