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 +118 -0
- package/dist/index.js.map +2 -2
- package/package.json +1 -1
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, [
|