infracensus-collector 1.3.0 → 1.4.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/dist/index.js CHANGED
@@ -15409,6 +15409,74 @@ ${tokenCss()}
15409
15409
  };
15410
15410
  };
15411
15411
 
15412
+ /**
15413
+ * PASTE A LIST OF LOGINS, GET ONE ROW EACH.
15414
+ *
15415
+ * Restored from the pre-migration console, where an operator kept their
15416
+ * device logins in a password manager or a spreadsheet and pasted the lot.
15417
+ * The row-at-a-time editor that replaced it asks for six clicks and twelve
15418
+ * tabs to enter four credentials, which is why people stopped entering the
15419
+ * second and third \u2014 and a sweep with one credential finds the devices that
15420
+ * credential opens and reports the rest as unreachable.
15421
+ *
15422
+ * \u2500\u2500 THE FORMAT IS FORGIVING ON PURPOSE \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
15423
+ *
15424
+ * One login per line, fields in the order username, password, enable. The
15425
+ * separator may be a comma, a colon, a tab, or runs of spaces, because that
15426
+ * is what the places people paste FROM actually produce: a CSV export, a
15427
+ * password manager\u2019s user:pass, a column copied out of a spreadsheet. A
15428
+ * parser that accepted exactly one of those would be a parser that rejected
15429
+ * most real pastes, and the operator would have no idea which one it wanted.
15430
+ *
15431
+ * \u2500\u2500 AND ITS ONE HONEST LIMIT \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
15432
+ *
15433
+ * The split stops after three fields, so every line is unambiguous EXCEPT
15434
+ * one whose PASSWORD contains the separator: pasted as admin:p@ss:w0rd
15435
+ * this reads as password p@ss and enable w0rd. Nothing in a flat line can
15436
+ * tell those apart, so the limit is stated rather than guessed at \u2014 the
15437
+ * hint on screen sends a password with a comma or colon in it to the rows
15438
+ * below, which is what the rows are still there for.
15439
+ *
15440
+ * A line with one field is a username with no password, which is a real
15441
+ * thing (key-based logins) and is kept rather than dropped.
15442
+ */
15443
+ function parsePastedLogins(text) {
15444
+ const out = [];
15445
+ for (const line of String(text || "").split(/\\r?\\n/)) {
15446
+ const row = line.trim();
15447
+ // Blank lines and comments: a pasted block often carries both.
15448
+ if (!row || row.charAt(0) === "#") continue;
15449
+ let parts;
15450
+ if (row.indexOf("\\t") >= 0) parts = row.split("\\t");
15451
+ else if (row.indexOf(",") >= 0) parts = splitLimited(row, ",", 3);
15452
+ else if (row.indexOf(":") >= 0) parts = splitLimited(row, ":", 3);
15453
+ else parts = row.split(/\\s+/);
15454
+ const username = (parts[0] || "").trim();
15455
+ if (!username) continue;
15456
+ out.push({
15457
+ username: username,
15458
+ // Not trimmed per field: whitespace INSIDE a line is kept, because a
15459
+ password: parts[1] === undefined ? "" : parts[1],
15460
+ enable: parts[2] === undefined ? "" : parts[2],
15461
+ });
15462
+ }
15463
+ return out;
15464
+ }
15465
+
15466
+ /** Split on the first (max - 1) separators; the remainder is the last field. */
15467
+ function splitLimited(text, sep, max) {
15468
+ const out = [];
15469
+ let rest = text;
15470
+ while (out.length < max - 1) {
15471
+ const at = rest.indexOf(sep);
15472
+ if (at < 0) break;
15473
+ out.push(rest.slice(0, at));
15474
+ rest = rest.slice(at + sep.length);
15475
+ }
15476
+ out.push(rest);
15477
+ return out;
15478
+ }
15479
+
15412
15480
  const trimmed = (v) => (typeof v === "string" ? v.trim() : v);
15413
15481
  const nonEmpty = (list) => list.filter((v) => v !== "" && v !== null && v !== undefined);
15414
15482
  const lines = (value) => nonEmpty(String(value || "").split(/\\r?\\n/).map((s) => s.trim()));
@@ -16395,6 +16463,56 @@ ${tokenCss()}
16395
16463
  const addSsh = el("button", { type: "button", class: "subtle", text: "Add an SSH login" });
16396
16464
  addSsh.addEventListener("click", () => sshRows.add());
16397
16465
  host.appendChild(addSsh);
16466
+
16467
+ // \u2500\u2500 PASTE THE LIST INSTEAD OF TYPING IT \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
16468
+ //
16469
+ // Four credentials is six clicks and twelve tabs through the rows above,
16470
+ // which is why operators stopped entering the second and third \u2014 and a
16471
+ // sweep carrying one credential finds the devices that credential opens
16472
+ // and reports every other device as unreachable. The rows stay: they are
16473
+ // how you fix one field, and how you enter a password with a comma in it.
16474
+ const pasteWrap = el("details", { style: "margin-top:8px" });
16475
+ const pasteSummary = el("summary", { style: "cursor:pointer",
16476
+ text: "Paste several logins at once" });
16477
+ pasteWrap.appendChild(pasteSummary);
16478
+ const pasteBox = el("textarea", { id: prefix + "SshPaste", spellcheck: "false", rows: "4",
16479
+ placeholder: "admin,P@ssw0rd,EnableSecret\\nreadonly:viewpass\\nsvc-net secret123" });
16480
+ pasteWrap.appendChild(field("One login per line \u2014 username, password, enable secret", pasteBox,
16481
+ "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."));
16482
+ const pasteBtn = el("button", { type: "button", class: "subtle", text: "Add these logins" });
16483
+ const pasteNote = el("div", { class: "hint" });
16484
+ pasteBtn.addEventListener("click", () => {
16485
+ const parsed = parsePastedLogins(pasteBox.value);
16486
+ if (!parsed.length) {
16487
+ pasteNote.textContent = "Nothing to add \u2014 one login per line, username first.";
16488
+ return;
16489
+ }
16490
+ // APPENDED, not replacing. Somebody who has typed two rows and then
16491
+ // pastes six means eight, and silently discarding the two would be the
16492
+ // kind of data loss that is only noticed when a scan comes back short.
16493
+ //
16494
+ // A row whose username is already listed is skipped rather than
16495
+ // duplicated: the SSH loop tries each credential in turn against every
16496
+ // host, so a duplicate is a second identical login attempt per device \u2014
16497
+ // which on a locked-down estate is how an account lockout policy trips.
16498
+ const already = sshRows.values().map((r) => trimmed(r.username)).filter(Boolean);
16499
+ let added = 0;
16500
+ let skipped = 0;
16501
+ for (const cred of parsed) {
16502
+ if (already.indexOf(cred.username) >= 0) { skipped += 1; continue; }
16503
+ already.push(cred.username);
16504
+ sshRows.add(cred);
16505
+ added += 1;
16506
+ }
16507
+ pasteBox.value = "";
16508
+ pasteNote.textContent =
16509
+ "Added " + added + " login" + (added === 1 ? "" : "s")
16510
+ + (skipped ? ", skipped " + skipped + " already listed by that username" : "")
16511
+ + ". They are tried in the order shown, top first.";
16512
+ });
16513
+ pasteWrap.appendChild(pasteBtn);
16514
+ pasteWrap.appendChild(pasteNote);
16515
+ host.appendChild(pasteWrap);
16398
16516
  host.appendChild(el("h3", { text: "Windows logins (WMI enrichment)" }));
16399
16517
  const win = el("div", null, []); host.appendChild(win);
16400
16518
  const winRows = rowsEditor(win, [