macwarden 1.1.0 → 1.1.1
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/CHANGELOG.md +50 -0
- package/README.md +7 -5
- package/SECURITY.md +2 -2
- package/dist/bounded-file.js +1 -1
- package/dist/commands/bake/out-errors.js +1 -0
- package/dist/commands/bake/write-out.js +1 -1
- package/dist/commands/handlers.js +1 -1
- package/dist/commands/inspect.js +1 -1
- package/dist/commands/inventory.js +1 -1
- package/dist/commands/recommend.js +1 -1
- package/dist/commands/respond.js +1 -1
- package/dist/commands/revoke.js +1 -1
- package/dist/commands/rotate.js +1 -1
- package/dist/commands/scan.js +1 -1
- package/dist/commands/verify.js +1 -1
- package/dist/commands/watch.js +1 -1
- package/dist/credential-recognition.js +1 -1
- package/dist/decode/index.js +1 -1
- package/dist/decode/recognition.js +1 -1
- package/dist/environment.js +1 -1
- package/dist/errors.js +1 -1
- package/dist/grade/index.js +1 -1
- package/dist/grouped-runs.js +1 -1
- package/dist/input/index.js +1 -1
- package/dist/input/lndconnect.js +1 -1
- package/dist/instant.js +1 -0
- package/dist/lnd/grpc.js +1 -1
- package/dist/lnd/index.js +1 -1
- package/dist/lnd/middleware-proto.js +1 -1
- package/dist/lnd/tls-identity.js +1 -1
- package/dist/online.js +1 -1
- package/dist/output/index.js +1 -1
- package/dist/output/scan-formats.js +1 -1
- package/dist/process-error-context.js +1 -1
- package/dist/protected-path.js +1 -1
- package/dist/registry/index.js +1 -1
- package/dist/registry/process-domain.js +1 -1
- package/dist/report/access-review.js +1 -1
- package/dist/root-key-id.js +1 -0
- package/dist/runtime.js +1923 -636
- package/dist/scan/agent-configs.js +1 -1
- package/dist/scan/git-history.js +1 -1
- package/dist/scan/ignore.js +1 -1
- package/dist/scan/index.js +1 -1
- package/dist/scan/mcp-context.js +1 -1
- package/dist/scan/nwc-match.js +1 -1
- package/dist/scan/patterns.js +1 -1
- package/dist/scan/redact.js +1 -1
- package/dist/secret.js +1 -1
- package/dist/version.js +1 -1
- package/dist/watch/alert-throttle.js +1 -1
- package/dist/watch/alert.js +1 -1
- package/dist/watch/canary.js +1 -1
- package/dist/watch/output-queue.js +1 -1
- package/dist/watch/usage.js +1 -1
- package/package.json +1 -1
- package/schemas/output/attenuate.schema.json +28 -1
- package/schemas/output/bake.schema.json +28 -1
- package/schemas/output/inspect.schema.json +7 -2
- package/schemas/output/inventory.schema.json +28 -1
- package/schemas/output/macwarden.schema.json +329 -0
- package/schemas/output/recommend.schema.json +28 -1
- package/schemas/output/respond.schema.json +28 -1
- package/schemas/output/revoke.schema.json +28 -1
- package/schemas/output/rotate.schema.json +28 -1
- package/schemas/output/scan.schema.json +8 -3
- package/schemas/output/verify.schema.json +28 -1
- package/schemas/output/watch.schema.json +28 -1
package/dist/runtime.js
CHANGED
|
@@ -5,22 +5,23 @@ import * as builtin3 from "node:path";
|
|
|
5
5
|
import * as builtin4 from "node:stream";
|
|
6
6
|
import * as builtin5 from "node:crypto";
|
|
7
7
|
import * as builtin6 from "node:child_process";
|
|
8
|
-
import * as builtin7 from "node:
|
|
9
|
-
import * as builtin8 from "node:
|
|
10
|
-
import * as builtin9 from "node:
|
|
8
|
+
import * as builtin7 from "node:readline";
|
|
9
|
+
import * as builtin8 from "node:util";
|
|
10
|
+
import * as builtin9 from "node:os";
|
|
11
11
|
import * as builtin10 from "node:http2";
|
|
12
12
|
import * as builtin11 from "node:https";
|
|
13
|
-
const builtins = {"node:fs": builtin0,"node:fs/promises": builtin1,"node:async_hooks": builtin2,"node:path": builtin3,"node:stream": builtin4,"node:crypto": builtin5,"node:child_process": builtin6,"node:
|
|
13
|
+
const builtins = {"node:fs": builtin0,"node:fs/promises": builtin1,"node:async_hooks": builtin2,"node:path": builtin3,"node:stream": builtin4,"node:crypto": builtin5,"node:child_process": builtin6,"node:readline": builtin7,"node:util": builtin8,"node:os": builtin9,"node:http2": builtin10,"node:https": builtin11};
|
|
14
14
|
const factories = {"bounded-file.js": (module, exports, require) => {
|
|
15
15
|
"use strict";
|
|
16
16
|
exports.__esModule = true;
|
|
17
17
|
exports.LOCK_FILE_LIMIT = exports.DEFAULT_FILE_LIMIT = exports.BoundedFileError = void 0;
|
|
18
18
|
exports.readBounded = readBounded;
|
|
19
19
|
exports.readBoundedFile = readBoundedFile;
|
|
20
|
-
exports.
|
|
21
|
-
// Bounded regular-file reads for files macwarden reads by path: TLS certificates
|
|
22
|
-
// sidecars kept next to the registry. The path and
|
|
23
|
-
//
|
|
20
|
+
exports.readPlainFile = readPlainFile;
|
|
21
|
+
// Bounded regular-file reads for files macwarden reads by path: TLS certificates, the
|
|
22
|
+
// sidecars kept next to the registry and lock owner records. The path (and, except for lock
|
|
23
|
+
// records, which must not be symlinks, its resolved target) is checked against the
|
|
24
|
+
// protected names (SAFE-02) before anything is opened, the opened file must be the
|
|
24
25
|
// regular file that was checked, and at most `limit` bytes are accepted: a larger file fails
|
|
25
26
|
// before its content is parsed, and allocation follows the file's size, never the limit.
|
|
26
27
|
const node_fs_1 = require("node:fs");
|
|
@@ -94,18 +95,39 @@ async function readBoundedFile(path, io, limit = exports.DEFAULT_FILE_LIMIT) {
|
|
|
94
95
|
/** Largest lock owner record read: a few fields (pid, host name, start time, process domain). */
|
|
95
96
|
exports.LOCK_FILE_LIMIT = 64 * 1024;
|
|
96
97
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
98
|
+
* Reads a file that must stay in place (lock owner records, whose reads are part of the lock
|
|
99
|
+
* protocol) through one descriptor: the path itself must be a regular file, not a symlink,
|
|
100
|
+
* under no protected name and at most `limit` bytes when checked; the opened file must be
|
|
101
|
+
* that same regular file (device and inode, compared as bigints so Windows file indexes stay
|
|
102
|
+
* exact; no-follow is used where the platform has it, and Windows has none, so the identity
|
|
103
|
+
* check is what binds the read to the checked file), and at most limit + 1 bytes are read
|
|
104
|
+
* from it. A symlink or protected path is refused before any open; a file replaced after the
|
|
105
|
+
* check is refused before its content is read; one that grew past `limit` fails once
|
|
106
|
+
* limit + 1 bytes have been read.
|
|
100
107
|
*/
|
|
101
|
-
async function
|
|
108
|
+
async function readPlainFile(path, io, limit) {
|
|
102
109
|
if ((0, protected_path_js_1.isProtectedPath)(path))
|
|
103
110
|
throw new BoundedFileError('protected file');
|
|
104
|
-
const meta = await io.lstat(path);
|
|
111
|
+
const meta = await io.lstat(path, { bigint: true });
|
|
105
112
|
if (!meta.isFile() || meta.isSymbolicLink())
|
|
106
113
|
throw new BoundedFileError('not a regular file');
|
|
107
|
-
if (meta.size > limit)
|
|
114
|
+
if (meta.size > BigInt(limit))
|
|
108
115
|
throw new BoundedFileError('file too large');
|
|
116
|
+
const handle = await io.open(path, node_fs_1.constants.O_RDONLY | (node_fs_1.constants.O_NONBLOCK ?? 0) | (node_fs_1.constants.O_NOFOLLOW ?? 0));
|
|
117
|
+
try {
|
|
118
|
+
const opened = await handle.stat({ bigint: true });
|
|
119
|
+
if (!opened.isFile() || opened.dev !== meta.dev || opened.ino !== meta.ino)
|
|
120
|
+
throw new BoundedFileError('file changed');
|
|
121
|
+
if (opened.size > BigInt(limit))
|
|
122
|
+
throw new BoundedFileError('file too large');
|
|
123
|
+
const bytes = await readBounded(handle, limit, opened.size);
|
|
124
|
+
if (bytes.length > limit)
|
|
125
|
+
throw new BoundedFileError('file too large');
|
|
126
|
+
return bytes.toString('utf8');
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
await handle.close();
|
|
130
|
+
}
|
|
109
131
|
}
|
|
110
132
|
|
|
111
133
|
},
|
|
@@ -445,11 +467,28 @@ async function main(argv) {
|
|
|
445
467
|
process.stdout.write((0, index_js_1.render)({ command: null, ok: true, warnings: [], result: { text: packageVersion() }, error: null }, 'text'));
|
|
446
468
|
return 0;
|
|
447
469
|
}
|
|
470
|
+
if (argv.length === 2 && argv.includes('--help') && argv.includes('--json')) {
|
|
471
|
+
// Top-level help as one JSON document (MW110-10); schemas/output/macwarden.schema.json.
|
|
472
|
+
const help = { command: null, ok: true, warnings: [], result: {
|
|
473
|
+
usage: usage.split('\n').slice(0, 3).map(line => line.replace(/^(?:Usage: | +)/, '')),
|
|
474
|
+
commands: summaries.map(([name, summary]) => ({ name, summary }))
|
|
475
|
+
}, error: null };
|
|
476
|
+
await write(help, 'json');
|
|
477
|
+
return 0;
|
|
478
|
+
}
|
|
448
479
|
if (argv.length === 1 && argv[0] === '--help') {
|
|
449
480
|
process.stdout.write((0, index_js_1.render)(textResult(usage), 'text'));
|
|
450
481
|
return 0;
|
|
451
482
|
}
|
|
452
483
|
if (command && argv.slice(1, flagsEnd).includes('--help')) {
|
|
484
|
+
// With --json, help is one JSON document like every other output (MW110-10).
|
|
485
|
+
if (mode === 'json') {
|
|
486
|
+
// Rendered only as JSON, so it goes through the same JSON writer without a per-command result type.
|
|
487
|
+
const help = { command, ok: true, warnings: [], result: { usage: synopses[command].map(line => `macwarden ${line}`) },
|
|
488
|
+
error: null };
|
|
489
|
+
await write(help, 'json');
|
|
490
|
+
return 0;
|
|
491
|
+
}
|
|
453
492
|
process.stdout.write((0, index_js_1.render)(textResult(commandUsage(command)), 'text'));
|
|
454
493
|
return 0;
|
|
455
494
|
}
|
|
@@ -562,6 +601,7 @@ const index_js_2 = require("../grade/index.js");
|
|
|
562
601
|
const index_js_3 = require("../input/index.js");
|
|
563
602
|
const protected_path_js_1 = require("../protected-path.js");
|
|
564
603
|
const write_out_js_1 = require("./bake/write-out.js");
|
|
604
|
+
const out_errors_js_1 = require("./bake/out-errors.js");
|
|
565
605
|
const valueFlags = new Set(['--out', '--expires', '--ip', '--ip-range']);
|
|
566
606
|
const switchFlags = new Set(['--dry-run', '--unsafe-arg', '--json']);
|
|
567
607
|
function fail(code, step, message, warnings = []) {
|
|
@@ -670,6 +710,9 @@ async function runSteps(args, deps, state) {
|
|
|
670
710
|
// A3: the output must be a new, unprotected path.
|
|
671
711
|
if ((0, protected_path_js_1.isProtectedPath)(request.out))
|
|
672
712
|
return fail(3, 'A3', 'protected output path');
|
|
713
|
+
const outName = (0, protected_path_js_1.outputNameProblem)(request.out);
|
|
714
|
+
if (outName)
|
|
715
|
+
return fail(3, 'A3', `output path refused: ${outName}`);
|
|
673
716
|
let outPath;
|
|
674
717
|
try {
|
|
675
718
|
outPath = await (0, protected_path_js_1.resolveUnprotectedPath)(request.out, deps.fs);
|
|
@@ -741,8 +784,8 @@ async function runSteps(args, deps, state) {
|
|
|
741
784
|
if (reserved.aclWarning)
|
|
742
785
|
warnings.push('acl-best-effort');
|
|
743
786
|
}
|
|
744
|
-
catch {
|
|
745
|
-
return fail(3, 'A6', 'cannot create output file', warnings);
|
|
787
|
+
catch (error) {
|
|
788
|
+
return fail(3, 'A6', (0, out_errors_js_1.reserveFailure)(error, 'cannot create output file'), warnings);
|
|
746
789
|
}
|
|
747
790
|
(0, cancellation_js_1.checkCancellation)();
|
|
748
791
|
try {
|
|
@@ -845,6 +888,7 @@ const process_error_context_js_1 = require("../../process-error-context.js");
|
|
|
845
888
|
const index_js_4 = require("../../registry/index.js");
|
|
846
889
|
const errors_js_1 = require("../../errors.js");
|
|
847
890
|
const write_out_js_1 = require("./write-out.js");
|
|
891
|
+
const out_errors_js_1 = require("./out-errors.js");
|
|
848
892
|
const allocate_js_1 = require("./allocate.js");
|
|
849
893
|
const canary_js_1 = require("../../watch/canary.js");
|
|
850
894
|
// A canary looks like an ordinary read-only credential. Its default permissions cover the
|
|
@@ -911,15 +955,15 @@ function parse(args, env) {
|
|
|
911
955
|
const raw = get('--permissions');
|
|
912
956
|
const uri = get('--uri');
|
|
913
957
|
const expires = get('--expires');
|
|
914
|
-
const host =
|
|
915
|
-
const cert = get('--tls-cert') ?? env.MACWARDEN_TLS_CERT;
|
|
916
|
-
const own = get('--macaroon') ?? env.MACWARDEN_MACAROON;
|
|
958
|
+
const { host, cert, own, problem: mixedSource } = (0, online_js_1.connectionSettings)(get, env);
|
|
917
959
|
if (!label)
|
|
918
960
|
return '--label is required';
|
|
919
961
|
if (!/^[A-Za-z][A-Za-z0-9._-]{0,63}$/.test(label))
|
|
920
962
|
return `--label ${online_js_1.LABEL_RULE}`;
|
|
921
963
|
if (!out)
|
|
922
964
|
return '--out is required: the path of the new macaroon file';
|
|
965
|
+
if (mixedSource)
|
|
966
|
+
return mixedSource;
|
|
923
967
|
const connection = (0, online_js_1.connectionProblem)(host, cert, own);
|
|
924
968
|
if (connection)
|
|
925
969
|
return connection;
|
|
@@ -975,6 +1019,9 @@ async function runSteps(args, deps, state) {
|
|
|
975
1019
|
return fail(2, 'B3', 'ADMIN permissions require --allow-admin');
|
|
976
1020
|
if ((0, protected_path_js_1.isProtectedPath)(request.out))
|
|
977
1021
|
return fail(3, 'B4', 'protected output path');
|
|
1022
|
+
const outName = (0, protected_path_js_1.outputNameProblem)(request.out);
|
|
1023
|
+
if (outName)
|
|
1024
|
+
return fail(3, 'B4', `output path refused: ${outName}`);
|
|
978
1025
|
let outPath;
|
|
979
1026
|
try {
|
|
980
1027
|
outPath = await (0, protected_path_js_1.resolveUnprotectedPath)(request.out, deps.fs);
|
|
@@ -1067,8 +1114,8 @@ async function runSteps(args, deps, state) {
|
|
|
1067
1114
|
if (reserved.aclWarning)
|
|
1068
1115
|
warnings.push('acl-best-effort');
|
|
1069
1116
|
}
|
|
1070
|
-
catch {
|
|
1071
|
-
return fail(3, 'B7',
|
|
1117
|
+
catch (error) {
|
|
1118
|
+
return fail(3, 'B7', (0, out_errors_js_1.reserveFailure)(error), warnings);
|
|
1072
1119
|
}
|
|
1073
1120
|
}
|
|
1074
1121
|
const adapter = deps.lnd ?? deps.createAdapter?.({ restUrl: request.host, tlsCert: cert, macaroon: own });
|
|
@@ -1113,14 +1160,34 @@ async function runSteps(args, deps, state) {
|
|
|
1113
1160
|
fingerprint: null, outPath: null, createdAt, verified: false, status: 'active',
|
|
1114
1161
|
revokedAt: null, revokeEvidence: null, replaces: null, replacedBy: null };
|
|
1115
1162
|
(0, cancellation_js_1.checkCancellation)();
|
|
1163
|
+
// The pending record may be on disk once save started (a failure after the rename, or a signal
|
|
1164
|
+
// during it), although nothing was sent to LND: it is removed again before reporting, so exit 3
|
|
1165
|
+
// and exit 6 keep meaning "nothing changed"; if the registry on disk still holds it, the exit
|
|
1166
|
+
// is 7 and the message names it with its recovery (RL-08).
|
|
1167
|
+
const unrecord = async (code, step, message) => {
|
|
1168
|
+
try {
|
|
1169
|
+
registry.removePending(pubkey, id);
|
|
1170
|
+
await registry.save();
|
|
1171
|
+
return fail(code, step, message, warnings);
|
|
1172
|
+
}
|
|
1173
|
+
catch { /* Decide from what is on disk. */ }
|
|
1174
|
+
try {
|
|
1175
|
+
if (!(await (0, index_js_4.openRegistry)(request.registry, { fs: deps.fs, env: deps.env })).find(pubkey, id))
|
|
1176
|
+
return fail(code, step, message, warnings);
|
|
1177
|
+
}
|
|
1178
|
+
catch { /* Unreadable: the record may remain. */ }
|
|
1179
|
+
return fail(7, step, `${message}; nothing sent to LND, but the registry still holds (or may hold) the pending record for root key ${id}` +
|
|
1180
|
+
`, which keeps the label in use; run macwarden inventory and, if it lists root key ${id}, ${(0, online_js_1.recoveryCommand)(request, id)}`, warnings);
|
|
1181
|
+
};
|
|
1116
1182
|
try {
|
|
1117
1183
|
registry.add(entry);
|
|
1118
1184
|
await registry.save();
|
|
1119
1185
|
}
|
|
1120
1186
|
catch {
|
|
1121
|
-
return
|
|
1187
|
+
return await unrecord(3, 'B11', 'cannot record pending issued ID; nothing sent to LND');
|
|
1122
1188
|
}
|
|
1123
|
-
(0, cancellation_js_1.
|
|
1189
|
+
if ((0, cancellation_js_1.cancellationSignal)()?.aborted)
|
|
1190
|
+
return await unrecord(6, 'cancelled', 'interrupted');
|
|
1124
1191
|
let mac;
|
|
1125
1192
|
state.mutationSent = true;
|
|
1126
1193
|
(0, process_error_context_js_1.markMutationSent)();
|
|
@@ -1274,6 +1341,22 @@ async function bake(args) {
|
|
|
1274
1341
|
return run(args, { fs: nodeFs, env: { ...process.env }, clock: () => new Date(), createAdapter: index_js_1.createLndAdapter });
|
|
1275
1342
|
}
|
|
1276
1343
|
|
|
1344
|
+
},
|
|
1345
|
+
"commands/bake/out-errors.js": (module, exports, require) => {
|
|
1346
|
+
"use strict";
|
|
1347
|
+
exports.__esModule = true;
|
|
1348
|
+
exports.OutputReserveError = void 0;
|
|
1349
|
+
exports.reserveFailure = reserveFailure;
|
|
1350
|
+
/** A reservation failure whose message the commands show after "cannot reserve output file". */
|
|
1351
|
+
class OutputReserveError extends Error {
|
|
1352
|
+
constructor(message) { super(message); this.name = 'OutputReserveError'; }
|
|
1353
|
+
}
|
|
1354
|
+
exports.OutputReserveError = OutputReserveError;
|
|
1355
|
+
/** The text a command prints for a failed output reservation. */
|
|
1356
|
+
function reserveFailure(error, fallback = 'cannot reserve output file') {
|
|
1357
|
+
return error instanceof OutputReserveError ? `${fallback}: ${error.message}` : fallback;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1277
1360
|
},
|
|
1278
1361
|
"commands/bake/write-out.js": (module, exports, require) => {
|
|
1279
1362
|
"use strict";
|
|
@@ -1315,60 +1398,32 @@ exports.reserveOut = reserveOut;
|
|
|
1315
1398
|
const secret_capability_js_1 = require("../../secret-capability.js");
|
|
1316
1399
|
const nodeFs = __importStar(require("node:fs/promises"));
|
|
1317
1400
|
const node_fs_1 = require("node:fs");
|
|
1318
|
-
const node_child_process_1 = require("node:child_process");
|
|
1319
1401
|
const node_path_1 = require("node:path");
|
|
1320
|
-
const
|
|
1321
|
-
const
|
|
1402
|
+
const protected_path_js_1 = require("../../protected-path.js");
|
|
1403
|
+
const node_child_process_1 = require("node:child_process");
|
|
1404
|
+
const out_errors_js_1 = require("./out-errors.js");
|
|
1322
1405
|
// libuv's UV_FS_O_EXLOCK: on Windows the file is opened with share mode 0, so the
|
|
1323
|
-
// open fails while any other handle exists and no
|
|
1324
|
-
// it is held. Node does not export the constant; libuv passes it through.
|
|
1406
|
+
// open fails while any other handle with data access exists, and no such handle can
|
|
1407
|
+
// be opened while it is held. Node does not export the constant; libuv passes it through.
|
|
1325
1408
|
const UV_FS_O_EXLOCK = 0x10000000;
|
|
1326
|
-
class OutputShareError extends
|
|
1327
|
-
constructor() { super('output file was opened by another process before it was
|
|
1328
|
-
}
|
|
1329
|
-
function run(file, args) {
|
|
1330
|
-
return new Promise(resolve => {
|
|
1331
|
-
(0, node_child_process_1.execFile)(file, args, { shell: false, windowsHide: true, timeout: 10_000 }, (error, stdout) => resolve({ ok: error === null, stdout: String(stdout ?? '') }));
|
|
1332
|
-
});
|
|
1333
|
-
}
|
|
1334
|
-
/** The current user's SID from System32 whoami, or null when unavailable or malformed. */
|
|
1335
|
-
async function currentUserSid(systemRoot) {
|
|
1336
|
-
const result = await run(node_path_1.win32.join(systemRoot, 'System32', 'whoami.exe'), ['/user', '/fo', 'csv', '/nh']);
|
|
1337
|
-
if (!result.ok)
|
|
1338
|
-
return null;
|
|
1339
|
-
const fields = /^"[^"\r\n]*","([^"\r\n]*)"$/.exec(result.stdout.trim());
|
|
1340
|
-
return fields && SID.test(fields[1]) ? fields[1] : null;
|
|
1409
|
+
class OutputShareError extends out_errors_js_1.OutputReserveError {
|
|
1410
|
+
constructor() { super('output file was opened or replaced by another process before it was written'); this.name = 'OutputShareError'; }
|
|
1341
1411
|
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Creates `path` exclusively for one credential (or report) and returns a reservation that
|
|
1414
|
+
* writes it through a handle no other process holds. The file is owner-only before any byte
|
|
1415
|
+
* is written, and that is verified; anything else fails before writing. `aclWarning` is true
|
|
1416
|
+
* only on a Windows volume that stores no permissions (FAT, exFAT).
|
|
1417
|
+
*/
|
|
1342
1418
|
async function reserveOut(path, fs = nodeFs) {
|
|
1343
|
-
|
|
1419
|
+
const nameProblem = (0, protected_path_js_1.outputNameProblem)(path);
|
|
1420
|
+
if (nameProblem)
|
|
1421
|
+
throw new out_errors_js_1.OutputReserveError(nameProblem);
|
|
1422
|
+
const reserved = process.platform === 'win32' ? await reserveWindows(path, fs) : await reservePosix(path, fs);
|
|
1423
|
+
const handle = reserved.handle;
|
|
1344
1424
|
let written = false;
|
|
1345
|
-
let aclWarning = false;
|
|
1346
|
-
if (process.platform === 'win32') {
|
|
1347
|
-
try {
|
|
1348
|
-
const systemRoot = process.env.SystemRoot ?? 'C:\\Windows';
|
|
1349
|
-
if (!node_path_1.win32.isAbsolute(systemRoot))
|
|
1350
|
-
throw new Error('invalid Windows system directory');
|
|
1351
|
-
// Prefer the SID: account names fail to resolve for some account types,
|
|
1352
|
-
// such as Microsoft and Entra ID accounts. Without a valid SID, use the
|
|
1353
|
-
// account name. One icacls call either way; its failure is the warning.
|
|
1354
|
-
const sid = await currentUserSid(systemRoot);
|
|
1355
|
-
const user = (0, node_os_1.userInfo)().username;
|
|
1356
|
-
const domain = process.env.USERDOMAIN;
|
|
1357
|
-
const principal = sid ? `*${sid}:F` : domain ? `${domain}\\${user}:F` : `${user}:F`;
|
|
1358
|
-
const applied = await run(node_path_1.win32.join(systemRoot, 'System32', 'icacls.exe'), [path, '/inheritance:r', '/grant:r', principal]);
|
|
1359
|
-
aclWarning = !applied.ok;
|
|
1360
|
-
}
|
|
1361
|
-
catch {
|
|
1362
|
-
aclWarning = true;
|
|
1363
|
-
}
|
|
1364
|
-
// Windows checks access only when a handle is opened, so a handle opened before
|
|
1365
|
-
// icacls keeps its access. Replace the creation handle with an exclusive one: it
|
|
1366
|
-
// cannot be opened while any other handle exists, so a successful open proves no
|
|
1367
|
-
// other process holds the file, and none can open it until the bytes are written.
|
|
1368
|
-
handle = await exclusiveReopen(path, handle, fs);
|
|
1369
|
-
}
|
|
1370
1425
|
return {
|
|
1371
|
-
aclWarning,
|
|
1426
|
+
aclWarning: reserved.aclWarning,
|
|
1372
1427
|
reservation: {
|
|
1373
1428
|
async write(mac) {
|
|
1374
1429
|
await handle.writeFile((0, secret_capability_js_1.revealSecret)(mac));
|
|
@@ -1390,31 +1445,225 @@ async function reserveOut(path, fs = nodeFs) {
|
|
|
1390
1445
|
},
|
|
1391
1446
|
};
|
|
1392
1447
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1448
|
+
// POSIX: create 0600 with O_EXCL, then prove the mode through the open descriptor. File
|
|
1449
|
+
// systems that cannot store modes (WSL drvfs, vfat, exFAT, CIFS) report other bits; chmod
|
|
1450
|
+
// is tried once and, if the file is still not private, it is removed and nothing is written.
|
|
1451
|
+
async function reservePosix(path, fs) {
|
|
1452
|
+
const handle = await fs.open(path, 'wx', 0o600);
|
|
1453
|
+
const uid = process.getuid?.();
|
|
1454
|
+
const private_ = async () => {
|
|
1455
|
+
const stat = await handle.stat();
|
|
1456
|
+
return (stat.mode & 0o077) === 0 && (uid === undefined || stat.uid === uid);
|
|
1457
|
+
};
|
|
1458
|
+
try {
|
|
1459
|
+
if (await private_())
|
|
1460
|
+
return { handle, aclWarning: false };
|
|
1461
|
+
await handle.chmod(0o600).catch(() => undefined);
|
|
1462
|
+
if (await private_())
|
|
1463
|
+
return { handle, aclWarning: false };
|
|
1464
|
+
}
|
|
1465
|
+
catch { /* checked below as not private */ }
|
|
1395
1466
|
try {
|
|
1396
|
-
|
|
1467
|
+
await handle.close();
|
|
1397
1468
|
}
|
|
1398
1469
|
finally {
|
|
1399
|
-
await
|
|
1470
|
+
await fs.unlink(path).catch(() => undefined);
|
|
1400
1471
|
}
|
|
1401
|
-
|
|
1472
|
+
throw new out_errors_js_1.OutputReserveError('the file system did not keep owner-only mode 0600; nothing was written');
|
|
1473
|
+
}
|
|
1474
|
+
// Windows: the fixed helper creates the file with an owner-only, protected DACL in the same
|
|
1475
|
+
// call that creates it, so no other principal is ever granted access and none can hold a
|
|
1476
|
+
// handle to it (not even one that only changes permissions). The file is then opened with
|
|
1477
|
+
// share mode 0, and the owner and DACL of the object at the path are read back while that
|
|
1478
|
+
// handle is held: the owner must be this user and the DACL must grant only this user.
|
|
1479
|
+
async function reserveWindows(path, fs) {
|
|
1480
|
+
const systemRoot = process.env.SystemRoot ?? 'C:\\Windows';
|
|
1481
|
+
if (!node_path_1.win32.isAbsolute(systemRoot))
|
|
1482
|
+
throw new out_errors_js_1.OutputReserveError('invalid Windows system directory');
|
|
1483
|
+
const creation = await createPrivateFile(path, systemRoot);
|
|
1484
|
+
if (!creation)
|
|
1485
|
+
throw new out_errors_js_1.OutputReserveError('could not create the file with owner-only permissions');
|
|
1486
|
+
let handle;
|
|
1402
1487
|
try {
|
|
1403
|
-
|
|
1488
|
+
handle = await fs.open(path, node_fs_1.constants.O_RDWR | UV_FS_O_EXLOCK);
|
|
1404
1489
|
}
|
|
1405
1490
|
catch {
|
|
1406
|
-
// Another handle exists (EBUSY) or the file
|
|
1407
|
-
|
|
1491
|
+
// Another handle exists (EBUSY) or the file is gone. Nothing has been written.
|
|
1492
|
+
creation.end();
|
|
1408
1493
|
await fs.unlink(path).catch(() => undefined);
|
|
1409
1494
|
throw new OutputShareError();
|
|
1410
1495
|
}
|
|
1411
|
-
|
|
1412
|
-
if (
|
|
1413
|
-
|
|
1414
|
-
|
|
1496
|
+
// FAT and exFAT keep no owner and no ACL, so there is nothing to verify.
|
|
1497
|
+
if (creation.noAcl) {
|
|
1498
|
+
creation.end();
|
|
1499
|
+
return { handle, aclWarning: true };
|
|
1500
|
+
}
|
|
1501
|
+
const found = await creation.verify();
|
|
1502
|
+
if (!found || found.owner !== creation.sid) {
|
|
1503
|
+
// Not provably the file the helper created: do not write to it or delete it.
|
|
1504
|
+
await handle.close();
|
|
1415
1505
|
throw new OutputShareError();
|
|
1416
1506
|
}
|
|
1417
|
-
|
|
1507
|
+
if (!ownerOnly(found, creation.sid)) {
|
|
1508
|
+
try {
|
|
1509
|
+
await handle.close();
|
|
1510
|
+
}
|
|
1511
|
+
finally {
|
|
1512
|
+
await fs.unlink(path).catch(() => undefined);
|
|
1513
|
+
}
|
|
1514
|
+
throw new out_errors_js_1.OutputReserveError('the file did not keep owner-only permissions (another account has access); nothing was written');
|
|
1515
|
+
}
|
|
1516
|
+
return { handle, aclWarning: false };
|
|
1517
|
+
}
|
|
1518
|
+
const SID = /^S-1-\d+(?:-\d+)+$/;
|
|
1519
|
+
// The fixed helper creates the file with CreateNew and a security descriptor that is
|
|
1520
|
+
// private from the first instant: owner = this user, a protected DACL (nothing inherited)
|
|
1521
|
+
// and one ACE granting this user full control. No other principal is ever granted access,
|
|
1522
|
+
// so none can open a handle to the file, including one that only changes its permissions.
|
|
1523
|
+
// The path arrives through the environment, never through the command text.
|
|
1524
|
+
const helperScript = String.raw `
|
|
1525
|
+
$ErrorActionPreference = 'Stop'
|
|
1526
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
1527
|
+
function Describe($sec) {
|
|
1528
|
+
$owner = $sec.GetOwner([Security.Principal.SecurityIdentifier])
|
|
1529
|
+
$rules = @($sec.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier]) | ForEach-Object {
|
|
1530
|
+
@{ sid = $_.IdentityReference.Value; allow = ($_.AccessControlType -eq 'Allow'); inherited = $_.IsInherited; rights = [int64]$_.FileSystemRights } })
|
|
1531
|
+
return @{ owner = $(if ($owner) { $owner.Value } else { $null }); protected = $sec.AreAccessRulesProtected; rules = $rules }
|
|
1532
|
+
}
|
|
1533
|
+
try {
|
|
1534
|
+
$path = $env:MACWARDEN_PRIVATE_OUT
|
|
1535
|
+
$me = [Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
1536
|
+
# FAT and exFAT volumes store no Windows permissions at all.
|
|
1537
|
+
$noAcl = $false
|
|
1538
|
+
try { $noAcl = (New-Object IO.DriveInfo([IO.Path]::GetPathRoot($path))).DriveFormat -match '^(FAT|FAT12|FAT16|FAT32|exFAT)$' } catch { $noAcl = $false }
|
|
1539
|
+
if ($noAcl) {
|
|
1540
|
+
$stream = New-Object IO.FileStream($path, [IO.FileMode]::CreateNew, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None)
|
|
1541
|
+
$stream.Dispose(); $created = $null
|
|
1542
|
+
} else {
|
|
1543
|
+
$sec = New-Object Security.AccessControl.FileSecurity
|
|
1544
|
+
$sec.SetOwner($me)
|
|
1545
|
+
$sec.SetAccessRuleProtection($true, $false)
|
|
1546
|
+
$sec.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule($me, [Security.AccessControl.FileSystemRights]::FullControl, [Security.AccessControl.AccessControlType]::Allow)))
|
|
1547
|
+
$stream = New-Object IO.FileStream($path, [IO.FileMode]::CreateNew, [Security.AccessControl.FileSystemRights]::FullControl, [IO.FileShare]::None, 4096, [IO.FileOptions]::None, $sec)
|
|
1548
|
+
try { $created = Describe $stream.GetAccessControl() } catch { $created = @{ unreadable = $true } } finally { $stream.Dispose() }
|
|
1549
|
+
}
|
|
1550
|
+
} catch { [Console]::Out.WriteLine('FAILED'); exit 1 }
|
|
1551
|
+
[Console]::Out.WriteLine('CREATED ' + (@{ sid = $me.Value; noAcl = $noAcl; acl = $created } | ConvertTo-Json -Compress -Depth 4)); [Console]::Out.Flush()
|
|
1552
|
+
if ([Console]::In.ReadLine() -ne 'verify') { exit 0 }
|
|
1553
|
+
try { $found = Describe ([IO.File]::GetAccessControl($path, [Security.AccessControl.AccessControlSections]'Owner, Access')) } catch { $found = $null }
|
|
1554
|
+
[Console]::Out.WriteLine('VERIFIED ' + (@{ acl = $found } | ConvertTo-Json -Compress -Depth 4)); [Console]::Out.Flush()
|
|
1555
|
+
`;
|
|
1556
|
+
function parseAcl(value) {
|
|
1557
|
+
if (!value || typeof value !== 'object')
|
|
1558
|
+
return null;
|
|
1559
|
+
const record = value;
|
|
1560
|
+
const rules = Array.isArray(record.rules) ? record.rules : record.rules && typeof record.rules === 'object' ? [record.rules] : null;
|
|
1561
|
+
if (!rules || typeof record.protected !== 'boolean')
|
|
1562
|
+
return null;
|
|
1563
|
+
const parsed = [];
|
|
1564
|
+
for (const rule of rules) {
|
|
1565
|
+
if (!rule || typeof rule.sid !== 'string' || typeof rule.allow !== 'boolean'
|
|
1566
|
+
|| typeof rule.inherited !== 'boolean' || typeof rule.rights !== 'number')
|
|
1567
|
+
return null;
|
|
1568
|
+
parsed.push({ sid: rule.sid, allow: rule.allow, inherited: rule.inherited, rights: rule.rights });
|
|
1569
|
+
}
|
|
1570
|
+
return { owner: typeof record.owner === 'string' ? record.owner : null, protected: record.protected, rules: parsed };
|
|
1571
|
+
}
|
|
1572
|
+
/** True when the ACL grants only `sid`, nothing is inherited, and `sid` owns the object. */
|
|
1573
|
+
function ownerOnly(acl, sid) {
|
|
1574
|
+
return !!acl && acl.owner === sid && acl.protected && acl.rules.length > 0
|
|
1575
|
+
&& acl.rules.every(rule => rule.sid === sid && rule.allow && !rule.inherited);
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Creates `path` (which must not exist) with an owner-only security descriptor through the
|
|
1579
|
+
* fixed System32 PowerShell helper, then keeps the helper waiting so the caller can verify
|
|
1580
|
+
* the object at the path after opening it exclusively. Null when the helper did not report
|
|
1581
|
+
* a created file (the path already existed, or the helper is unavailable or failed).
|
|
1582
|
+
*/
|
|
1583
|
+
function createPrivateFile(path, systemRoot) {
|
|
1584
|
+
const executable = node_path_1.win32.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
1585
|
+
return new Promise(resolve => {
|
|
1586
|
+
let buffered = '';
|
|
1587
|
+
let exited = false;
|
|
1588
|
+
const waiting = [];
|
|
1589
|
+
const complete = (prefix) => buffered.split(/\r?\n/).slice(0, -1).find(line => line.startsWith(prefix));
|
|
1590
|
+
const deliver = () => {
|
|
1591
|
+
for (const item of [...waiting]) {
|
|
1592
|
+
const line = complete(item.prefix);
|
|
1593
|
+
if (line !== undefined || exited) {
|
|
1594
|
+
waiting.splice(waiting.indexOf(item), 1);
|
|
1595
|
+
item.settle(line === undefined ? null : line.slice(item.prefix.length));
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
const next = (prefix) => new Promise(settle => { waiting.push({ prefix, settle }); deliver(); });
|
|
1600
|
+
let child;
|
|
1601
|
+
try {
|
|
1602
|
+
child = (0, node_child_process_1.execFile)(executable, ['-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand',
|
|
1603
|
+
Buffer.from(helperScript, 'utf16le').toString('base64')], {
|
|
1604
|
+
shell: false, windowsHide: true, timeout: 20_000, maxBuffer: 64 * 1024, encoding: 'utf8',
|
|
1605
|
+
env: { SystemRoot: systemRoot, WINDIR: systemRoot, PATH: node_path_1.win32.join(systemRoot, 'System32'),
|
|
1606
|
+
PSModulePath: node_path_1.win32.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'Modules'),
|
|
1607
|
+
MACWARDEN_PRIVATE_OUT: path },
|
|
1608
|
+
}, (_error, stdout) => {
|
|
1609
|
+
// The callback carries the complete output; streamed chunks may not all have arrived.
|
|
1610
|
+
if (typeof stdout === 'string' && stdout.length > buffered.length)
|
|
1611
|
+
buffered = stdout;
|
|
1612
|
+
buffered += '\n';
|
|
1613
|
+
exited = true;
|
|
1614
|
+
deliver();
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
catch {
|
|
1618
|
+
child = undefined;
|
|
1619
|
+
}
|
|
1620
|
+
if (!child?.stdout || !child.stdin) {
|
|
1621
|
+
resolve(null);
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
const stdin = child.stdin;
|
|
1625
|
+
stdin.on('error', () => undefined);
|
|
1626
|
+
child.stdout.on('data', (chunk) => { if (!exited) {
|
|
1627
|
+
buffered += String(chunk);
|
|
1628
|
+
deliver();
|
|
1629
|
+
} });
|
|
1630
|
+
const end = () => { try {
|
|
1631
|
+
stdin.end();
|
|
1632
|
+
}
|
|
1633
|
+
catch { /* the helper already exited */ } };
|
|
1634
|
+
void next('CREATED ').then(line => {
|
|
1635
|
+
let reply;
|
|
1636
|
+
try {
|
|
1637
|
+
reply = line === null ? null : JSON.parse(line);
|
|
1638
|
+
}
|
|
1639
|
+
catch {
|
|
1640
|
+
reply = null;
|
|
1641
|
+
}
|
|
1642
|
+
if (!reply || typeof reply.sid !== 'string' || !SID.test(reply.sid)) {
|
|
1643
|
+
end();
|
|
1644
|
+
resolve(null);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
let verified;
|
|
1648
|
+
resolve({ sid: reply.sid, noAcl: reply.noAcl === true, created: parseAcl(reply.acl), end,
|
|
1649
|
+
verify: () => verified ??= (async () => {
|
|
1650
|
+
try {
|
|
1651
|
+
stdin.write('verify\n');
|
|
1652
|
+
}
|
|
1653
|
+
catch {
|
|
1654
|
+
return null;
|
|
1655
|
+
}
|
|
1656
|
+
const found = await next('VERIFIED ');
|
|
1657
|
+
end();
|
|
1658
|
+
try {
|
|
1659
|
+
return found === null ? null : parseAcl(JSON.parse(found).acl);
|
|
1660
|
+
}
|
|
1661
|
+
catch {
|
|
1662
|
+
return null;
|
|
1663
|
+
}
|
|
1664
|
+
})() });
|
|
1665
|
+
});
|
|
1666
|
+
});
|
|
1418
1667
|
}
|
|
1419
1668
|
|
|
1420
1669
|
},
|
|
@@ -1624,6 +1873,7 @@ const usage_js_1 = require("../watch/usage.js");
|
|
|
1624
1873
|
const access_review_js_1 = require("../report/access-review.js");
|
|
1625
1874
|
const version_js_1 = require("../version.js");
|
|
1626
1875
|
const write_out_js_1 = require("./bake/write-out.js");
|
|
1876
|
+
const out_errors_js_1 = require("./bake/out-errors.js");
|
|
1627
1877
|
const methods = [
|
|
1628
1878
|
['/lnrpc.Lightning/GetInfo', 'info:read'],
|
|
1629
1879
|
['/lnrpc.Lightning/ListPermissions', 'info:read'],
|
|
@@ -1707,9 +1957,14 @@ async function runSteps(args, deps) {
|
|
|
1707
1957
|
return fail(2, null, '--out needs --report <format>');
|
|
1708
1958
|
if (reportOut !== undefined && (0, protected_path_js_1.isProtectedPath)(reportOut))
|
|
1709
1959
|
return fail(3, 'N7', 'protected report path');
|
|
1710
|
-
const
|
|
1711
|
-
|
|
1712
|
-
|
|
1960
|
+
const reportName = reportOut === undefined ? null : (0, protected_path_js_1.outputNameProblem)(reportOut);
|
|
1961
|
+
if (reportName)
|
|
1962
|
+
return fail(3, 'N7', `report path refused: ${reportName}`);
|
|
1963
|
+
const settings = (0, online_js_1.connectionSettings)(flag => supplied[flag], deps.env);
|
|
1964
|
+
if (settings.problem)
|
|
1965
|
+
return fail(2, null, settings.problem);
|
|
1966
|
+
// connectionProblem below refuses a missing setting before either path is used.
|
|
1967
|
+
const host = settings.host, certPath = settings.cert, ownPath = settings.own;
|
|
1713
1968
|
const connection = (0, online_js_1.connectionProblem)(host, certPath, ownPath);
|
|
1714
1969
|
if (connection)
|
|
1715
1970
|
return fail(2, null, connection);
|
|
@@ -1935,8 +2190,8 @@ async function runSteps(args, deps) {
|
|
|
1935
2190
|
throw error;
|
|
1936
2191
|
}
|
|
1937
2192
|
}
|
|
1938
|
-
catch {
|
|
1939
|
-
return fail(3, 'N7', 'cannot write report file', warnings);
|
|
2193
|
+
catch (error) {
|
|
2194
|
+
return fail(3, 'N7', (0, out_errors_js_1.reserveFailure)(error, 'cannot write report file'), warnings);
|
|
1940
2195
|
}
|
|
1941
2196
|
path = (0, node_path_1.resolve)(reportOut);
|
|
1942
2197
|
}
|
|
@@ -2009,6 +2264,7 @@ const online_js_1 = require("../online.js");
|
|
|
2009
2264
|
const index_js_3 = require("../output/index.js");
|
|
2010
2265
|
const protected_path_js_1 = require("../protected-path.js");
|
|
2011
2266
|
const index_js_4 = require("../registry/index.js");
|
|
2267
|
+
const root_key_id_js_1 = require("../root-key-id.js");
|
|
2012
2268
|
const usage_js_1 = require("../watch/usage.js");
|
|
2013
2269
|
function fail(code, step, message, warnings = []) {
|
|
2014
2270
|
return { schema: 1, command: 'recommend', ok: false, warnings, result: null, error: { code, step, message } };
|
|
@@ -2054,12 +2310,12 @@ async function runSteps(args, deps) {
|
|
|
2054
2310
|
return fail(2, 'R1', 'give exactly one label, root key ID or credential ID (id:…), or --target-file <file>');
|
|
2055
2311
|
const selector = operands[0] ?? '';
|
|
2056
2312
|
const byCredential = /^id:[0-9a-f]{16}$/.test(selector);
|
|
2057
|
-
const byId =
|
|
2313
|
+
const byId = (0, root_key_id_js_1.isRootKeyId)(selector);
|
|
2058
2314
|
if (targetFile === undefined && !byCredential && !byId && !/^[A-Za-z][A-Za-z0-9._-]{0,63}$/.test(selector))
|
|
2059
2315
|
return fail(2, 'R1', 'selector must be a label, a root key ID or id:<16 hex>');
|
|
2060
|
-
const host = flags.get(
|
|
2061
|
-
|
|
2062
|
-
|
|
2316
|
+
const { host, cert: certPath, own: ownPath, problem: mixedSource } = (0, online_js_1.connectionSettings)(flag => flags.get(flag), deps.env);
|
|
2317
|
+
if (mixedSource)
|
|
2318
|
+
return fail(2, 'R1', mixedSource);
|
|
2063
2319
|
if (!host)
|
|
2064
2320
|
return fail(2, 'R1', 'missing --lnd-rest (or MACWARDEN_LND_REST)');
|
|
2065
2321
|
if (!certPath)
|
|
@@ -2241,28 +2497,43 @@ async function runSteps(args, deps) {
|
|
|
2241
2497
|
// without one, the credential existed no later than its first recorded use. Two bounds,
|
|
2242
2498
|
// the larger is taken: the recorded spans after creation (from 1.1.0), and the covered
|
|
2243
2499
|
// total minus the most that can lie before creation (all time from the first registration
|
|
2244
|
-
// to it). Files written before 1.0.1 record no coverage.
|
|
2500
|
+
// to it). Files written before 1.0.1 record no coverage. Every step must be finite: the
|
|
2501
|
+
// usage reader refuses impossible or reversed times, and if evidence that cannot be placed
|
|
2502
|
+
// on the clock still got this far, the window is invalid rather than silently long.
|
|
2245
2503
|
const coverage = usage.coverageFor(pubkey);
|
|
2246
2504
|
let seconds = null;
|
|
2505
|
+
let invalid = false;
|
|
2247
2506
|
if (coverage) {
|
|
2507
|
+
const start = Date.parse(coverage.firstStart), end = Date.parse(coverage.lastEnd);
|
|
2248
2508
|
const firstUse = records.length ? records.reduce((first, item) => Math.min(first, Date.parse(item.firstSeen)), Infinity) : null;
|
|
2249
2509
|
const created = entry ? Date.parse(entry.createdAt) : firstUse;
|
|
2250
|
-
const since = Math.max(
|
|
2251
|
-
const before = Math.ceil((since -
|
|
2252
|
-
const
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2510
|
+
const since = Math.max(start, created ?? -Infinity);
|
|
2511
|
+
const before = Math.ceil((since - start) / 1_000);
|
|
2512
|
+
const spanned = usage.coveredSince(pubkey, since);
|
|
2513
|
+
const lower = Math.max(coverage.coveredSeconds - before, spanned ?? 0);
|
|
2514
|
+
const bounded = Math.min(lower, Math.floor((end - since) / 1_000));
|
|
2515
|
+
invalid = ![start, end, since, before, lower, bounded, firstUse ?? 0, spanned ?? 0].every(Number.isFinite) || end < start;
|
|
2516
|
+
if (!invalid)
|
|
2517
|
+
seconds = Math.max(0, bounded);
|
|
2518
|
+
}
|
|
2519
|
+
// An invalid window is never accepted: --accept-short-observation accepts a short window,
|
|
2520
|
+
// not one that cannot be computed.
|
|
2521
|
+
const short = invalid || seconds === null || seconds < exports.SHORT_OBSERVATION_SECONDS;
|
|
2256
2522
|
const observation = { seconds, since: coverage?.firstStart ?? null, until: coverage?.lastEnd ?? null,
|
|
2257
|
-
sessions: coverage?.sessions ?? 0, short, accepted: short && acceptShort };
|
|
2258
|
-
if (
|
|
2523
|
+
sessions: coverage?.sessions ?? 0, short, accepted: short && acceptShort && !invalid };
|
|
2524
|
+
if (invalid && observed.length)
|
|
2525
|
+
warnings.push({ code: 'observation-invalid', message: 'the observation window cannot be computed '
|
|
2526
|
+
+ 'from the coverage record in the usage file (its times are not valid instants in order); no narrowing command is printed, even with '
|
|
2527
|
+
+ '--accept-short-observation. Stop watch, move the usage file aside and run watch again' });
|
|
2528
|
+
else if (short && observed.length)
|
|
2259
2529
|
warnings.push({ code: 'short-observation', message: `usage was observed for ${seconds === null
|
|
2260
2530
|
? 'an unknown time (this usage file has no watch coverage record)' : (0, index_js_3.formatDuration)(seconds)}, less than ${(0, index_js_3.formatDuration)(exports.SHORT_OBSERVATION_SECONDS)}; `
|
|
2261
2531
|
+ 'methods the app calls rarely (such as payments) may be missing, and a credential narrowed to this list would fail on them. '
|
|
2262
2532
|
+ (acceptShort ? 'The narrowing command is printed because --accept-short-observation was given'
|
|
2263
2533
|
: 'Keep watch running longer, or pass --accept-short-observation to print the narrowing command anyway') });
|
|
2264
2534
|
const commands = [];
|
|
2265
|
-
if (observed.length && !unmapped.length && !incomplete && !unverified && !beyond.length &&
|
|
2535
|
+
if (observed.length && !unmapped.length && !incomplete && !unverified && !beyond.length && !invalid
|
|
2536
|
+
&& (!short || acceptShort)) {
|
|
2266
2537
|
const label = entry?.label ? narrowLabel(entry.label) : 'narrowed';
|
|
2267
2538
|
const connection = `--lnd-rest ${quote(host)} --tls-cert ${quote((0, online_js_1.recoveryPath)(certPath))} --macaroon ${quote((0, online_js_1.recoveryPath)(ownPath))}` +
|
|
2268
2539
|
(registryPath ? ` --registry ${quote((0, online_js_1.recoveryPath)(registryPath))}` : '');
|
|
@@ -2337,13 +2608,17 @@ const index_js_1 = require("../lnd/index.js");
|
|
|
2337
2608
|
const index_js_2 = require("../decode/index.js");
|
|
2338
2609
|
const protected_path_js_1 = require("../protected-path.js");
|
|
2339
2610
|
const environment_js_1 = require("../environment.js");
|
|
2340
|
-
const index_js_3 = require("../
|
|
2341
|
-
const index_js_4 = require("../
|
|
2611
|
+
const index_js_3 = require("../grade/index.js");
|
|
2612
|
+
const index_js_4 = require("../registry/index.js");
|
|
2613
|
+
const index_js_5 = require("../output/index.js");
|
|
2342
2614
|
const rotate_js_1 = require("./rotate.js");
|
|
2343
2615
|
const valueFlags = new Set(['--registry', '--lnd-rest', '--tls-cert', '--macaroon', '--out-dir']);
|
|
2344
2616
|
const switchFlags = new Set(['--finish', '--dry-run', '--yes', '--allow-admin', '--break-lock', '--json']);
|
|
2345
2617
|
const methods = [
|
|
2346
2618
|
['/lnrpc.Lightning/GetInfo', 'info:read'],
|
|
2619
|
+
// Snapshot drift, as on every online command, checked by respond itself (MW110-09): nested
|
|
2620
|
+
// rotations cannot supply it when nothing is rotated (a dry run, an empty batch).
|
|
2621
|
+
['/lnrpc.Lightning/ListPermissions', 'info:read'],
|
|
2347
2622
|
['/lnrpc.Lightning/ListMacaroonIDs', 'macaroon:read'],
|
|
2348
2623
|
['/lnrpc.Lightning/CheckMacaroonPermissions', 'macaroon:read'],
|
|
2349
2624
|
];
|
|
@@ -2357,7 +2632,7 @@ exports.ROOT_KEY_0_GUIDANCE = [
|
|
|
2357
2632
|
exports.OWN_ON_ROOT_KEY_0 = 'macwarden\'s own credential is on root key ID 0: after the change it stops working too. Bake a new own macaroon from the regenerated admin.macaroon before running macwarden again.';
|
|
2358
2633
|
exports.REVOKED_BUT_PRESENT_GUIDANCE = 'The registry records these root key IDs as revoked, but LND still holds them. Either an earlier revoke did not complete, or someone holding a credential that can bake (macaroon:generate) created credentials on them again. Treat them as live and revoke each: macwarden revoke <id>.';
|
|
2359
2634
|
exports.BAKING_RISK_GUIDANCE = [
|
|
2360
|
-
'These credentials can bake new macaroons (macaroon:generate). If one leaked, its holder can create credentials on any root key ID, including the replacements respond stages and macwarden\'s own ID. Revoking the old root key IDs does not remove credentials baked that way.',
|
|
2635
|
+
'These credentials can bake new macaroons (macaroon:generate or uri:/lnrpc.Lightning/BakeMacaroon), or hold permissions macwarden could not assess. If one leaked, its holder can create credentials on any root key ID, including the replacements respond stages and macwarden\'s own ID. Revoking the old root key IDs does not remove credentials baked that way.',
|
|
2361
2636
|
'respond --finish revokes these first; keep the time between staging and finishing short. If a credential that can bake may have leaked, regenerate all root keys instead (see the root key ID 0 guidance): only that invalidates everything an attacker could have baked.',
|
|
2362
2637
|
'Each PROVEN result covers that credential only: its old root key ID is gone and its replacement is accepted. It does not prove that no other credential exists.',
|
|
2363
2638
|
];
|
|
@@ -2390,9 +2665,9 @@ function parse(args, env) {
|
|
|
2390
2665
|
const finish = flags.has('--finish');
|
|
2391
2666
|
if (finish && (flags.has('--out-dir') || flags.has('--allow-admin')))
|
|
2392
2667
|
return '--finish takes neither --out-dir nor --allow-admin';
|
|
2393
|
-
const hostArg =
|
|
2394
|
-
|
|
2395
|
-
|
|
2668
|
+
const { host: hostArg, cert, own, problem: mixedSource } = (0, online_js_1.connectionSettings)(get, env);
|
|
2669
|
+
if (mixedSource)
|
|
2670
|
+
return mixedSource;
|
|
2396
2671
|
if (!hostArg)
|
|
2397
2672
|
return 'missing --lnd-rest (or MACWARDEN_LND_REST)';
|
|
2398
2673
|
if (!cert)
|
|
@@ -2412,10 +2687,9 @@ function parse(args, env) {
|
|
|
2412
2687
|
allowAdmin: flags.has('--allow-admin'), breakLock: flags.has('--break-lock') };
|
|
2413
2688
|
}
|
|
2414
2689
|
const byId = (left, right) => { const a = BigInt(left), b = BigInt(right); return a < b ? -1 : a > b ? 1 : 0; };
|
|
2415
|
-
/** Active entries that are not themselves a staged replacement of another active entry. */
|
|
2690
|
+
/** Active entries that are not themselves a staged replacement of another active entry (as rotate S3; RL-07). */
|
|
2416
2691
|
function originals(entries) {
|
|
2417
|
-
return entries.filter(entry => entry.status === 'active' && !(
|
|
2418
|
-
&& entries.some(old => old.status === 'active' && old.rootKeyId === entry.replaces)));
|
|
2692
|
+
return entries.filter(entry => entry.status === 'active' && !(0, index_js_4.isStagedReplacement)(entries, entry));
|
|
2419
2693
|
}
|
|
2420
2694
|
function stagedReplacement(entries, old) {
|
|
2421
2695
|
return entries.find(entry => entry.status === 'active' && entry.replaces === old.rootKeyId);
|
|
@@ -2496,7 +2770,7 @@ async function runSteps(args, deps) {
|
|
|
2496
2770
|
try {
|
|
2497
2771
|
if (request.registry && (0, protected_path_js_1.isProtectedPath)(request.registry))
|
|
2498
2772
|
return fail(3, 'P3', 'protected registry path');
|
|
2499
|
-
entries = (await (0,
|
|
2773
|
+
entries = (await (0, index_js_4.openRegistry)(request.registry, { fs: deps.fs, env: deps.env })).entries;
|
|
2500
2774
|
}
|
|
2501
2775
|
catch {
|
|
2502
2776
|
return fail(3, 'P3', 'unreadable or invalid registry', warnings);
|
|
@@ -2509,6 +2783,8 @@ async function runSteps(args, deps) {
|
|
|
2509
2783
|
let ids;
|
|
2510
2784
|
try {
|
|
2511
2785
|
node = (0, online_js_1.nodeInfo)(await lnd.getInfo());
|
|
2786
|
+
if ((0, online_js_1.drift)(await lnd.listPermissions()))
|
|
2787
|
+
warnings.push('snapshot-drift');
|
|
2512
2788
|
ids = await (0, online_js_1.listIds)(lnd);
|
|
2513
2789
|
}
|
|
2514
2790
|
catch (error) {
|
|
@@ -2523,14 +2799,14 @@ async function runSteps(args, deps) {
|
|
|
2523
2799
|
// A staged replacement is believable only if macwarden could have written it; anything else
|
|
2524
2800
|
// is tampering and is reported instead of trusted (it could hide a credential from the batch).
|
|
2525
2801
|
// A chain or cycle of links would otherwise make originals() drop live credentials.
|
|
2526
|
-
const tampered = (0,
|
|
2802
|
+
const tampered = (0, index_js_4.replacementLinkProblems)(mine)[0];
|
|
2527
2803
|
if (tampered)
|
|
2528
2804
|
return fail(3, 'P5', `registry tampering: root key ID ${tampered.rootKeyId} claims to replace ${tampered.replaces}, but ${tampered.problem}; restore the registry from a trusted copy`, warnings);
|
|
2529
2805
|
// Whether a credential is a canary is decided by its file, not by the editable registry caveat list:
|
|
2530
2806
|
// a custom caveat the registry does not record would let the batch rotate a canary into an
|
|
2531
2807
|
// ordinary credential. Refused before any mutation, like a forged replacement link.
|
|
2532
2808
|
for (const entry of originals(mine)) {
|
|
2533
|
-
if ((0,
|
|
2809
|
+
if ((0, index_js_4.hasCustomCaveat)(entry))
|
|
2534
2810
|
continue;
|
|
2535
2811
|
const file = await readCredential(entry.outPath, deps);
|
|
2536
2812
|
if (file && file.facts.fingerprint === entry.fingerprint && file.facts.caveats.some(caveat => caveat.type === 'lnd-custom'))
|
|
@@ -2540,7 +2816,12 @@ async function runSteps(args, deps) {
|
|
|
2540
2816
|
// Only active entries cover an ID LND holds; a revoked entry for a present ID needs action.
|
|
2541
2817
|
const active = new Set(mine.filter(entry => entry.status === 'active').map(entry => entry.rootKeyId));
|
|
2542
2818
|
const revokedIds = new Set(mine.filter(entry => entry.status === 'revoked').map(entry => entry.rootKeyId));
|
|
2543
|
-
|
|
2819
|
+
// Baking authority, not the ADMIN grade (MW110-06): macaroon:generate or the BakeMacaroon URI. The
|
|
2820
|
+
// recorded grade is editable, so the permissions are also assessed here; anything unassessed may bake.
|
|
2821
|
+
const canBake = (entry) => {
|
|
2822
|
+
const assessed = (0, index_js_3.grade)(entry.permissions);
|
|
2823
|
+
return (0, index_js_3.canBake)(entry.permissions, entry.grade === 'UNASSESSED' ? 'UNASSESSED' : assessed.grade, assessed.warnings);
|
|
2824
|
+
};
|
|
2544
2825
|
// revoke resolves a label among this node's active entries; where that is ambiguous, advice names the ID.
|
|
2545
2826
|
const target = (entry) => entry.label !== null
|
|
2546
2827
|
&& mine.filter(other => other.status === 'active' && other.label === entry.label).length === 1 ? entry.label : entry.rootKeyId;
|
|
@@ -2554,7 +2835,7 @@ async function runSteps(args, deps) {
|
|
|
2554
2835
|
items.push({ ...base, action: 'manual', status: 'manual', note: entry.rootKeyId === ownId
|
|
2555
2836
|
? 'macwarden\'s own credential: bake a new own macaroon, switch MACWARDEN_MACAROON to it, then revoke this ID'
|
|
2556
2837
|
: entry.rootKeyId === '0' ? 'root key ID 0: see the root key ID 0 guidance' : 'no label: revoke it by root key ID, then bake a replacement' });
|
|
2557
|
-
else if ((0,
|
|
2838
|
+
else if ((0, index_js_4.hasCustomCaveat)(entry))
|
|
2558
2839
|
items.push({ ...base, action: 'manual', status: 'manual', note: exports.CUSTOM_CAVEAT_NOTE.replace('<label>', target(entry)) });
|
|
2559
2840
|
// A replacement an earlier stage allocated but never verified or wrote (an uncertain bake, a failed
|
|
2560
2841
|
// verification or write, a failed registry update) is unresolved work, not a usable stage. It is
|
|
@@ -2642,6 +2923,12 @@ async function runSteps(args, deps) {
|
|
|
2642
2923
|
const outDir = (0, node_path_1.resolve)(request.outDir);
|
|
2643
2924
|
if ((0, protected_path_js_1.isProtectedPath)(outDir))
|
|
2644
2925
|
return fail(3, 'P6', 'protected output directory', warnings);
|
|
2926
|
+
// Every replacement path is checked before anything is created, so no label can fail
|
|
2927
|
+
// after earlier replacements were written (a label such as CON maps to a Windows device).
|
|
2928
|
+
const outName = (0, protected_path_js_1.outputNameProblem)(outDir)
|
|
2929
|
+
?? selected.map(item => (0, protected_path_js_1.outputNameProblem)((0, node_path_1.join)(outDir, `${item.label}.macaroon`))).find(problem => problem !== null) ?? null;
|
|
2930
|
+
if (outName)
|
|
2931
|
+
return fail(3, 'P6', `output path refused: ${outName}`, warnings);
|
|
2645
2932
|
try {
|
|
2646
2933
|
await deps.fs.mkdir(outDir, { mode: 0o700 });
|
|
2647
2934
|
}
|
|
@@ -2706,7 +2993,7 @@ async function runSteps(args, deps) {
|
|
|
2706
2993
|
// P9: independent proof after every finish: the old ID is gone and the replacement is accepted.
|
|
2707
2994
|
if (request.finish && !interrupted) {
|
|
2708
2995
|
const after = await (0, online_js_1.listIds)(lnd).then(list => new Set(list.map(id => id.toString())), () => null);
|
|
2709
|
-
const current = await (0,
|
|
2996
|
+
const current = await (0, index_js_4.openRegistry)(request.registry, { fs: deps.fs, env: deps.env }).then(view => view.entries).catch(() => mine);
|
|
2710
2997
|
for (const item of selected.filter(entry => entry.status === 'finished')) {
|
|
2711
2998
|
const old = current.find(entry => entry.nodePubkey.toLowerCase() === pubkey && entry.rootKeyId === item.rootKeyId);
|
|
2712
2999
|
const next = current.find(entry => entry.nodePubkey.toLowerCase() === pubkey && entry.rootKeyId === item.replacementRootKeyId);
|
|
@@ -2744,7 +3031,7 @@ async function respond(args) {
|
|
|
2744
3031
|
return run(args, { fs: nodeFs, env: { ...process.env }, clock: () => new Date(), createAdapter: index_js_1.createLndAdapter,
|
|
2745
3032
|
stdin: process.stdin,
|
|
2746
3033
|
prompt: async (question) => {
|
|
2747
|
-
(0,
|
|
3034
|
+
(0, index_js_5.writeRotatePrompt)(question);
|
|
2748
3035
|
const input = (0, node_readline_1.createInterface)({ input: process.stdin, terminal: false });
|
|
2749
3036
|
const signal = (0, cancellation_js_1.cancellationSignal)();
|
|
2750
3037
|
const close = () => input.close();
|
|
@@ -2814,8 +3101,8 @@ const index_js_4 = require("../grade/index.js");
|
|
|
2814
3101
|
const protected_path_js_1 = require("../protected-path.js");
|
|
2815
3102
|
const environment_js_1 = require("../environment.js");
|
|
2816
3103
|
const process_error_context_js_1 = require("../process-error-context.js");
|
|
3104
|
+
const root_key_id_js_1 = require("../root-key-id.js");
|
|
2817
3105
|
const index_js_5 = require("../output/index.js");
|
|
2818
|
-
const maxId = (1n << 64n) - 1n;
|
|
2819
3106
|
const valueFlags = new Set(['--target-file', '--registry', '--lnd-rest', '--tls-cert', '--macaroon']);
|
|
2820
3107
|
const switchFlags = new Set(['--unregistered', '--yes', '--break-lock', '--dry-run', '--json']);
|
|
2821
3108
|
const requiredMethods = [
|
|
@@ -2861,11 +3148,9 @@ function parse(args, env) {
|
|
|
2861
3148
|
if (target && /^[0-9]+$/.test(target)) {
|
|
2862
3149
|
if (/^0+$/.test(target))
|
|
2863
3150
|
return 'root key ID 0 cannot be revoked: it is shared by the default macaroons (SAFE-03)';
|
|
2864
|
-
if (
|
|
3151
|
+
if (!(0, root_key_id_js_1.isRootKeyId)(target))
|
|
2865
3152
|
return targetRule;
|
|
2866
3153
|
id = BigInt(target);
|
|
2867
|
-
if (id > maxId)
|
|
2868
|
-
return targetRule;
|
|
2869
3154
|
}
|
|
2870
3155
|
else if (!target || !/^[A-Za-z][A-Za-z0-9._-]{0,63}$/.test(target))
|
|
2871
3156
|
return targetRule;
|
|
@@ -2873,10 +3158,10 @@ function parse(args, env) {
|
|
|
2873
3158
|
const value = flags.get(name);
|
|
2874
3159
|
return typeof value === 'string' ? value : undefined;
|
|
2875
3160
|
};
|
|
2876
|
-
const host =
|
|
2877
|
-
const cert = get('--tls-cert') ?? env.MACWARDEN_TLS_CERT;
|
|
2878
|
-
const own = get('--macaroon') ?? env.MACWARDEN_MACAROON;
|
|
3161
|
+
const { host, cert, own, problem: mixedSource } = (0, online_js_1.connectionSettings)(get, env);
|
|
2879
3162
|
const targetFile = get('--target-file');
|
|
3163
|
+
if (mixedSource)
|
|
3164
|
+
return mixedSource;
|
|
2880
3165
|
const connection = (0, online_js_1.connectionProblem)(host, cert, own);
|
|
2881
3166
|
if (connection)
|
|
2882
3167
|
return connection;
|
|
@@ -3007,10 +3292,14 @@ async function runSteps(args, deps, warnings, state) {
|
|
|
3007
3292
|
}
|
|
3008
3293
|
else {
|
|
3009
3294
|
entry = local.find(row => BigInt(row.rootKeyId) === request.id);
|
|
3010
|
-
|
|
3011
|
-
|
|
3295
|
+
// Root key IDs are per node (small IDs from lncli bakemacaroon --root_key_id exist on every
|
|
3296
|
+
// node), so an entry for another node says nothing about this node's ID: without
|
|
3297
|
+
// --unregistered the operator may have meant that node and is told so; with it, this node's
|
|
3298
|
+
// ID is revoked and recorded under this node's pubkey (RL-04).
|
|
3012
3299
|
if (!entry && !request.unregistered)
|
|
3013
|
-
return fail(2, 'V6',
|
|
3300
|
+
return fail(2, 'V6', entries.some(row => BigInt(row.rootKeyId) === request.id)
|
|
3301
|
+
? 'root key ID is registered only for another node; check --lnd-rest, or pass --unregistered to revoke this node\'s root key ID'
|
|
3302
|
+
: 'unregistered root key ID requires --unregistered; other tools and other operators share the same LND store, including Lightning Terminal', warnings);
|
|
3014
3303
|
}
|
|
3015
3304
|
const id = entry ? BigInt(entry.rootKeyId) : request.id;
|
|
3016
3305
|
const inspection = `root key ID ${id}; run \`${(0, online_js_1.inventoryRecoveryCommand)(request)}\` ` +
|
|
@@ -3029,9 +3318,13 @@ async function runSteps(args, deps, warnings, state) {
|
|
|
3029
3318
|
catch {
|
|
3030
3319
|
target = undefined;
|
|
3031
3320
|
}
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3321
|
+
const relation = target && (0, index_js_3.recordedFileRelation)(entry, target.facts);
|
|
3322
|
+
if (relation === 'tampered')
|
|
3323
|
+
return fail(3, 'V6', 'default target file carries the registry entry\'s fingerprint ' +
|
|
3324
|
+
'on another root key ID: the registry entry was edited; restore the registry from a trusted copy', warnings);
|
|
3325
|
+
// Another credential at a reused path, or another macaroon on this root key ID, is not
|
|
3326
|
+
// the target: the revocation is proven by ID only (RL-01).
|
|
3327
|
+
if (relation !== 'match')
|
|
3035
3328
|
target = undefined;
|
|
3036
3329
|
}
|
|
3037
3330
|
let ids;
|
|
@@ -3079,8 +3372,8 @@ async function runSteps(args, deps, warnings, state) {
|
|
|
3079
3372
|
try {
|
|
3080
3373
|
const current = await (0, index_js_3.openRegistry)(request.registry, { fs: deps.fs, env: deps.env });
|
|
3081
3374
|
const resolved = current.find(pubkey, id.toString());
|
|
3082
|
-
|
|
3083
|
-
|
|
3375
|
+
// Node-scoped like V6: an entry for this ID on another node is not this target (RL-04).
|
|
3376
|
+
if (!(0, node_util_1.isDeepStrictEqual)(resolved, entry))
|
|
3084
3377
|
return fail(3, 'V10', 'resolved registry entry changed since V6; nothing sent to LND', warnings);
|
|
3085
3378
|
// Retain the lock owner while preserving unrelated entries from the reread.
|
|
3086
3379
|
registry.reload(current.entries);
|
|
@@ -3128,6 +3421,10 @@ async function runSteps(args, deps, warnings, state) {
|
|
|
3128
3421
|
return fail(7, 'V12', `root key ID still present after deletion; registry unchanged; ${inspection}`, warnings);
|
|
3129
3422
|
}
|
|
3130
3423
|
if (proof === 'rejected') {
|
|
3424
|
+
// A signature mismatch alone is no proof (RT-01: an altered copy gets it too). Here it is
|
|
3425
|
+
// corroborating evidence only: both branches above return unless ListMacaroonIDs shows the
|
|
3426
|
+
// ID absent, and proof 'rejected' means V8 saw these exact bytes accepted in this run, so a
|
|
3427
|
+
// mismatch now can only come from LND's state for this ID, not from altered bytes.
|
|
3131
3428
|
try {
|
|
3132
3429
|
const checked = await lnd.checkMacaroon(target.secret, target.facts.permissions);
|
|
3133
3430
|
if (checked.valid !== false || (checked.reason !== 'root-key-missing' && checked.reason !== 'signature-mismatch'))
|
|
@@ -3264,6 +3561,7 @@ const index_js_4 = require("../registry/index.js");
|
|
|
3264
3561
|
const errors_js_1 = require("../errors.js");
|
|
3265
3562
|
const index_js_5 = require("../output/index.js");
|
|
3266
3563
|
const write_out_js_1 = require("./bake/write-out.js");
|
|
3564
|
+
const out_errors_js_1 = require("./bake/out-errors.js");
|
|
3267
3565
|
const allocate_js_1 = require("./bake/allocate.js");
|
|
3268
3566
|
const valueFlags = new Set(['--out', '--registry', '--lnd-rest', '--tls-cert', '--macaroon']);
|
|
3269
3567
|
const switchFlags = new Set(['--allow-admin', '--break-lock', '--dry-run', '--json', '--finish', '--yes']);
|
|
@@ -3323,9 +3621,9 @@ function parse(args, env) {
|
|
|
3323
3621
|
return '--yes is only used with --finish; the stage run asks no confirmation it can skip';
|
|
3324
3622
|
if (!label)
|
|
3325
3623
|
return 'give exactly one label';
|
|
3326
|
-
const host =
|
|
3327
|
-
|
|
3328
|
-
|
|
3624
|
+
const { host, cert, own, problem: mixedSource } = (0, online_js_1.connectionSettings)(get, env);
|
|
3625
|
+
if (mixedSource)
|
|
3626
|
+
return mixedSource;
|
|
3329
3627
|
const connection = (0, online_js_1.connectionProblem)(host, cert, own);
|
|
3330
3628
|
if (connection)
|
|
3331
3629
|
return connection;
|
|
@@ -3353,10 +3651,54 @@ function tampering(entries, old) {
|
|
|
3353
3651
|
const CUSTOM_CAVEAT_REFUSAL = 'this credential carries a custom caveat (a canary, or another lnd-custom caveat); a rotation ' +
|
|
3354
3652
|
'would replace it with an ordinary credential. Revoke it with macwarden revoke <label>, then bake a new canary with ' +
|
|
3355
3653
|
'macwarden bake --canary and restart macwarden watch';
|
|
3654
|
+
const OLD_FILE_TAMPERED = 'old credential file carries the registry entry\'s fingerprint on another root key ID: ' +
|
|
3655
|
+
'the registry entry was edited; restore the registry from a trusted copy';
|
|
3656
|
+
/** Active entries with this label that are not a staged replacement (SPEC 6 S3; RL-07). */
|
|
3356
3657
|
function originals(entries, label) {
|
|
3357
|
-
return entries.filter(entry => entry.label === label && entry.status === 'active'
|
|
3358
|
-
|
|
3359
|
-
|
|
3658
|
+
return entries.filter(entry => entry.label === label && entry.status === 'active' && !(0, index_js_4.isStagedReplacement)(entries, entry));
|
|
3659
|
+
}
|
|
3660
|
+
/**
|
|
3661
|
+
* The original for this label (RL-04). One registry may hold several nodes, and bake allows the
|
|
3662
|
+
* same label on each, so a label active on several nodes is resolved against the node macwarden
|
|
3663
|
+
* is connected to: its own credential and TLS certificate are read and GetInfo (read-only) names
|
|
3664
|
+
* the node. Returns the node's candidates, or a failure at `step`.
|
|
3665
|
+
*/
|
|
3666
|
+
async function nodeCandidates(candidates, request, deps, step) {
|
|
3667
|
+
if (new Set(candidates.map(entry => entry.nodePubkey.toLowerCase())).size < 2)
|
|
3668
|
+
return candidates;
|
|
3669
|
+
let cert;
|
|
3670
|
+
let own;
|
|
3671
|
+
try {
|
|
3672
|
+
if ((0, protected_path_js_1.isProtectedPath)(request.cert) || (0, protected_path_js_1.isProtectedPath)(await deps.fs.realpath(request.cert)))
|
|
3673
|
+
return fail(4, step, 'unreadable TLS certificate');
|
|
3674
|
+
cert = await (0, online_js_1.readBoundedFile)(request.cert, deps.fs);
|
|
3675
|
+
}
|
|
3676
|
+
catch {
|
|
3677
|
+
return fail(4, step, 'unreadable TLS certificate');
|
|
3678
|
+
}
|
|
3679
|
+
try {
|
|
3680
|
+
own = (await (0, online_js_1.readOnlineCredential)(request.own, { stdin: deps.stdin ?? process.stdin, fs: deps.fs })).secret;
|
|
3681
|
+
}
|
|
3682
|
+
catch {
|
|
3683
|
+
return fail(4, step, 'unreadable own macaroon');
|
|
3684
|
+
}
|
|
3685
|
+
try {
|
|
3686
|
+
(0, index_js_2.decodeMacaroon)(own);
|
|
3687
|
+
}
|
|
3688
|
+
catch {
|
|
3689
|
+
return fail(4, step, 'undecodable own macaroon');
|
|
3690
|
+
}
|
|
3691
|
+
const adapter = deps.lnd ?? deps.createAdapter?.({ restUrl: request.host, tlsCert: cert, macaroon: own });
|
|
3692
|
+
if (!adapter)
|
|
3693
|
+
throw new Error('missing LND adapter');
|
|
3694
|
+
let pubkey;
|
|
3695
|
+
try {
|
|
3696
|
+
pubkey = (0, online_js_1.nodePubkey)(await (0, online_js_1.onlineAdapter)(adapter).getInfo());
|
|
3697
|
+
}
|
|
3698
|
+
catch (error) {
|
|
3699
|
+
return lndFailure(error, step, []);
|
|
3700
|
+
}
|
|
3701
|
+
return candidates.filter(entry => entry.nodePubkey.toLowerCase() === pubkey);
|
|
3360
3702
|
}
|
|
3361
3703
|
async function readTarget(path, deps) {
|
|
3362
3704
|
if ((0, protected_path_js_1.isProtectedPath)(path) || (0, protected_path_js_1.isProtectedPath)(await deps.fs.realpath(path)))
|
|
@@ -3396,9 +3738,9 @@ async function finishSteps(request, deps, state) {
|
|
|
3396
3738
|
catch {
|
|
3397
3739
|
return fail(3, 'F3', 'unreadable or invalid registry');
|
|
3398
3740
|
}
|
|
3399
|
-
const candidates = originals(registry.entries, request.label);
|
|
3400
|
-
if (candidates
|
|
3401
|
-
return
|
|
3741
|
+
const candidates = await nodeCandidates(originals(registry.entries, request.label), request, deps, 'F3');
|
|
3742
|
+
if (!Array.isArray(candidates))
|
|
3743
|
+
return candidates;
|
|
3402
3744
|
const old = candidates[0];
|
|
3403
3745
|
const forged = old && tampering(registry.entries, old);
|
|
3404
3746
|
if (forged)
|
|
@@ -3504,9 +3846,12 @@ async function finishSteps(request, deps, state) {
|
|
|
3504
3846
|
target = await readTarget(old.outPath, deps);
|
|
3505
3847
|
}
|
|
3506
3848
|
catch { /* Without a usable old file, ID absence is the proof. */ }
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3849
|
+
const relation = target && (0, index_js_4.recordedFileRelation)(old, target.facts);
|
|
3850
|
+
if (relation === 'tampered')
|
|
3851
|
+
return fail(3, 'F7', OLD_FILE_TAMPERED, warnings);
|
|
3852
|
+
// A replacement staged at O's old path, or another macaroon on O's root key ID, is not O's
|
|
3853
|
+
// file: the old credential is then proven revoked by ID only, as in revoke (RL-01).
|
|
3854
|
+
if (relation !== 'match')
|
|
3510
3855
|
target = undefined;
|
|
3511
3856
|
if (target && usable(target, deps.clock())) {
|
|
3512
3857
|
try {
|
|
@@ -3635,9 +3980,9 @@ async function runSteps(args, deps, state) {
|
|
|
3635
3980
|
catch {
|
|
3636
3981
|
return fail(3, 'S3', 'unreadable or invalid registry');
|
|
3637
3982
|
}
|
|
3638
|
-
const candidates = originals(registry.entries, request.label);
|
|
3639
|
-
if (candidates
|
|
3640
|
-
return
|
|
3983
|
+
const candidates = await nodeCandidates(originals(registry.entries, request.label), request, deps, 'S3');
|
|
3984
|
+
if (!Array.isArray(candidates))
|
|
3985
|
+
return candidates;
|
|
3641
3986
|
if (!candidates.length)
|
|
3642
3987
|
return fail(2, 'S3', 'no eligible active entry for this label');
|
|
3643
3988
|
const old = candidates[0];
|
|
@@ -3663,10 +4008,15 @@ async function runSteps(args, deps, state) {
|
|
|
3663
4008
|
}
|
|
3664
4009
|
catch { /* An unusable old file falls back to the recorded permissions. */ }
|
|
3665
4010
|
}
|
|
4011
|
+
const relation = oldTarget && (0, index_js_4.recordedFileRelation)(old, oldTarget.facts);
|
|
4012
|
+
if (relation === 'tampered')
|
|
4013
|
+
return fail(3, 'S4', OLD_FILE_TAMPERED);
|
|
4014
|
+
// Another credential at a reused path proves nothing about O: O's recorded permissions then
|
|
4015
|
+
// need the informed approval below, as for a missing file (RL-01).
|
|
4016
|
+
if (relation === 'other-credential')
|
|
4017
|
+
oldTarget = undefined;
|
|
3666
4018
|
const oldFacts = oldTarget?.facts;
|
|
3667
|
-
if (
|
|
3668
|
-
return fail(3, 'S4', 'old credential file root key ID differs from the registry entry');
|
|
3669
|
-
if (oldFacts && oldFacts.fingerprint !== old.fingerprint)
|
|
4019
|
+
if (relation === 'same-root-key')
|
|
3670
4020
|
return fail(3, 'S4', 'old credential fingerprint differs from the registry entry');
|
|
3671
4021
|
// The registry's caveat list is editable metadata: the credential itself decides whether it is a
|
|
3672
4022
|
// canary. A custom caveat the registry does not record is tampering, refused before any mutation.
|
|
@@ -3699,8 +4049,22 @@ async function runSteps(args, deps, state) {
|
|
|
3699
4049
|
return fail(2, 'S5', `unassessed permissions: ${assessment.unassessed.join(', ')}`);
|
|
3700
4050
|
if (assessment.grade === 'ADMIN' && !request.allowAdmin)
|
|
3701
4051
|
return fail(2, 'S5', 'ADMIN permissions require --allow-admin');
|
|
4052
|
+
// S11 reuses O's recorded expiry duration: it is checked here, before any lock or LND call,
|
|
4053
|
+
// so a bad value can never leave a baked root key behind (RL-03).
|
|
4054
|
+
const expires = old.caveats.find(caveat => caveat.type === 'time-before' && caveat.requested !== undefined)?.requested;
|
|
4055
|
+
if (expires !== undefined) {
|
|
4056
|
+
try {
|
|
4057
|
+
(0, index_js_2.timeBeforeCondition)(expires, deps.clock);
|
|
4058
|
+
}
|
|
4059
|
+
catch {
|
|
4060
|
+
return fail(3, 'S5', 'registry entry records an invalid expiry duration; restore the registry from a trusted copy');
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
3702
4063
|
if ((0, protected_path_js_1.isProtectedPath)(request.out))
|
|
3703
4064
|
return fail(3, 'S5', 'protected output path');
|
|
4065
|
+
const outName = (0, protected_path_js_1.outputNameProblem)(request.out);
|
|
4066
|
+
if (outName)
|
|
4067
|
+
return fail(3, 'S5', `output path refused: ${outName}`);
|
|
3704
4068
|
let outPath;
|
|
3705
4069
|
try {
|
|
3706
4070
|
outPath = await (0, protected_path_js_1.resolveUnprotectedPath)(request.out, deps.fs);
|
|
@@ -3777,8 +4141,8 @@ async function runSteps(args, deps, state) {
|
|
|
3777
4141
|
if (reserved.aclWarning)
|
|
3778
4142
|
warnings.push('acl-best-effort');
|
|
3779
4143
|
}
|
|
3780
|
-
catch {
|
|
3781
|
-
return fail(3, 'S7',
|
|
4144
|
+
catch (error) {
|
|
4145
|
+
return fail(3, 'S7', (0, out_errors_js_1.reserveFailure)(error), warnings);
|
|
3782
4146
|
}
|
|
3783
4147
|
}
|
|
3784
4148
|
const adapter = deps.lnd ?? deps.createAdapter?.({ restUrl: request.host, tlsCert: cert, macaroon: own });
|
|
@@ -3835,20 +4199,42 @@ async function runSteps(args, deps, state) {
|
|
|
3835
4199
|
const id = rootKeyId.toString();
|
|
3836
4200
|
state.inspection = `new root key ID ${id}; old credential unchanged; ` +
|
|
3837
4201
|
`run \`${(0, online_js_1.inventoryRecoveryCommand)(request)}\` to inspect state; recover with ${(0, online_js_1.recoveryCommand)(request, id)}`;
|
|
3838
|
-
|
|
4202
|
+
// Never before O (RL-02): a clock behind O's recorded creation time must not make this
|
|
4203
|
+
// link look like tampering to the finish, respond and inventory checks.
|
|
4204
|
+
const createdAt = new Date(Math.max(deps.clock().getTime(), Date.parse(old.createdAt))).toISOString();
|
|
3839
4205
|
const entry = { nodePubkey: pubkey, label: request.label, rootKeyId: id,
|
|
3840
4206
|
permissions: [...permissions], caveats: [], grade: assessment.grade, fingerprint: null,
|
|
3841
4207
|
outPath: null, createdAt, verified: false, status: 'active', revokedAt: null, revokeEvidence: null,
|
|
3842
4208
|
replaces: old.rootKeyId, replacedBy: null };
|
|
3843
4209
|
(0, cancellation_js_1.checkCancellation)();
|
|
4210
|
+
// The pending record may be on disk once save started (a failure after the rename, or a signal
|
|
4211
|
+
// during it), although nothing was sent to LND: it is removed again before reporting, so exit 3
|
|
4212
|
+
// and exit 6 keep meaning "nothing changed"; if the registry on disk still holds it, the exit
|
|
4213
|
+
// is 7 and the message names it with its recovery (RL-08).
|
|
4214
|
+
const unrecord = async (code, step, message) => {
|
|
4215
|
+
try {
|
|
4216
|
+
registry.removePending(pubkey, id);
|
|
4217
|
+
await registry.save();
|
|
4218
|
+
return fail(code, step, message, warnings);
|
|
4219
|
+
}
|
|
4220
|
+
catch { /* Decide from what is on disk. */ }
|
|
4221
|
+
try {
|
|
4222
|
+
if (!(await (0, index_js_4.openRegistry)(request.registry, { fs: deps.fs, env: deps.env })).find(pubkey, id))
|
|
4223
|
+
return fail(code, step, message, warnings);
|
|
4224
|
+
}
|
|
4225
|
+
catch { /* Unreadable: the record may remain. */ }
|
|
4226
|
+
return fail(7, step, `${message}; nothing sent to LND, but the registry still holds (or may hold) the pending record for root key ${id}` +
|
|
4227
|
+
`; run \`${(0, online_js_1.inventoryRecoveryCommand)(request)}\` and, if it lists root key ${id}, ${(0, online_js_1.recoveryCommand)(request, id)}`, warnings);
|
|
4228
|
+
};
|
|
3844
4229
|
try {
|
|
3845
4230
|
registry.add(entry);
|
|
3846
4231
|
await registry.save();
|
|
3847
4232
|
}
|
|
3848
4233
|
catch {
|
|
3849
|
-
return
|
|
4234
|
+
return await unrecord(3, 'S10', 'cannot record pending issued ID; nothing sent to LND');
|
|
3850
4235
|
}
|
|
3851
|
-
(0, cancellation_js_1.
|
|
4236
|
+
if ((0, cancellation_js_1.cancellationSignal)()?.aborted)
|
|
4237
|
+
return await unrecord(6, 'cancelled', 'interrupted');
|
|
3852
4238
|
let mac;
|
|
3853
4239
|
state.mutationSent = true;
|
|
3854
4240
|
(0, process_error_context_js_1.markMutationSent)();
|
|
@@ -3907,7 +4293,6 @@ async function runSteps(args, deps, state) {
|
|
|
3907
4293
|
return await recover('S11', 'bake response root key ID or permissions differ from the request');
|
|
3908
4294
|
entry.permissions = [...finalFacts.permissions];
|
|
3909
4295
|
entry.caveats = finalFacts.caveats.map(({ type, value }) => ({ type, value }));
|
|
3910
|
-
const expires = old.caveats.find(caveat => caveat.type === 'time-before' && caveat.requested !== undefined)?.requested;
|
|
3911
4296
|
try {
|
|
3912
4297
|
mac = expires !== undefined ? (0, index_js_2.addFirstPartyCaveat)(mac, (0, index_js_2.timeBeforeCondition)(expires, deps.clock))
|
|
3913
4298
|
: (0, index_js_2.encodeMacaroon)(finalFacts);
|
|
@@ -4211,7 +4596,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
4211
4596
|
};
|
|
4212
4597
|
})();
|
|
4213
4598
|
exports.__esModule = true;
|
|
4214
|
-
exports.CUSTOM_CAVEAT_REJECTED = exports.REJECTED_INCONCLUSIVE = exports.OWN_ROOT_KEY_ACCEPTED = exports.CAN_BAKE_ROOT_KEY_0 = exports.CAN_BAKE_ACCEPTED = exports.ROOT_KEY_0_ACCEPTED = void 0;
|
|
4599
|
+
exports.CUSTOM_CAVEAT_REJECTED = exports.ROOT_KEY_0_INCONCLUSIVE = exports.OWN_ROOT_KEY_INCONCLUSIVE = exports.SIGNATURE_MISMATCH_ROOT_KEY_0 = exports.SIGNATURE_MISMATCH_HOW_TO_CONCLUDE = exports.SIGNATURE_MISMATCH_INCONCLUSIVE = exports.LOCAL_EXPIRY_UNCONFIRMED = exports.REJECTED_INCONCLUSIVE = exports.OWN_ROOT_KEY_ACCEPTED = exports.CAN_BAKE_ROOT_KEY_0 = exports.CAN_BAKE_ACCEPTED = exports.ROOT_KEY_0_ACCEPTED = void 0;
|
|
4215
4600
|
exports.run = run;
|
|
4216
4601
|
exports.verify = verify;
|
|
4217
4602
|
const nodeFs = __importStar(require("node:fs/promises"));
|
|
@@ -4221,8 +4606,9 @@ const environment_js_1 = require("../environment.js");
|
|
|
4221
4606
|
const protected_path_js_1 = require("../protected-path.js");
|
|
4222
4607
|
const index_js_1 = require("../decode/index.js");
|
|
4223
4608
|
const index_js_2 = require("../input/index.js");
|
|
4224
|
-
const index_js_3 = require("../
|
|
4225
|
-
const index_js_4 = require("../
|
|
4609
|
+
const index_js_3 = require("../grade/index.js");
|
|
4610
|
+
const index_js_4 = require("../lnd/index.js");
|
|
4611
|
+
const index_js_5 = require("../registry/index.js");
|
|
4226
4612
|
const inspect_js_1 = require("./inspect.js");
|
|
4227
4613
|
const valueFlags = ['--registry', '--lnd-rest', '--tls-cert', '--macaroon'];
|
|
4228
4614
|
// GetInfo (node identity for the registry match), ListPermissions (snapshot drift, as on every
|
|
@@ -4240,10 +4626,16 @@ exports.ROOT_KEY_0_ACCEPTED = [
|
|
|
4240
4626
|
"That also invalidates every other macaroon on the node at once: every app's credential, macwarden's own and any replacement staged with macwarden respond. So regenerate first, in a maintenance window, then bake a fresh per-app macaroon for each app (macwarden bake) and re-pair it. Run macwarden inventory beforehand to list the apps that will need one; staging replacements before the regeneration would only see them invalidated too.",
|
|
4241
4627
|
'Procedure: docs/incident-playbook.md and docs/replacing-default-macaroons.md.',
|
|
4242
4628
|
];
|
|
4243
|
-
exports.CAN_BAKE_ACCEPTED = 'This credential can bake macaroons (macaroon:generate). If it leaked, its holder may have created credentials on other root key IDs, and revoking this one does not remove them. Replace and revoke every registered credential with macwarden respond, list unknown root key IDs with macwarden inventory, and if this credential may have been used, regenerate all root keys (see docs/incident-playbook.md).';
|
|
4244
|
-
exports.CAN_BAKE_ROOT_KEY_0 = 'It can also bake macaroons (macaroon:generate): its holder may have created credentials on other root key IDs. The regeneration invalidates those too; afterwards, revoke any root key ID macwarden inventory still shows as unknown that no app of yours uses.';
|
|
4629
|
+
exports.CAN_BAKE_ACCEPTED = 'This credential can bake macaroons (macaroon:generate or uri:/lnrpc.Lightning/BakeMacaroon), or macwarden could not assess all of its permissions. If it leaked, its holder may have created credentials on other root key IDs, and revoking this one does not remove them. Replace and revoke every registered credential with macwarden respond, list unknown root key IDs with macwarden inventory, and if this credential may have been used, regenerate all root keys (see docs/incident-playbook.md).';
|
|
4630
|
+
exports.CAN_BAKE_ROOT_KEY_0 = 'It can also bake macaroons (macaroon:generate or uri:/lnrpc.Lightning/BakeMacaroon), or may (not all of its permissions could be assessed): its holder may have created credentials on other root key IDs. The regeneration invalidates those too; afterwards, revoke any root key ID macwarden inventory still shows as unknown that no app of yours uses.';
|
|
4245
4631
|
exports.OWN_ROOT_KEY_ACCEPTED = "This credential shares the root key ID of the credential macwarden is running with, and macwarden revoke refuses its own root key ID. Revoke it with another own credential, or delete that root key ID with lncli deletemacaroonid after switching macwarden to a new session macaroon.";
|
|
4246
|
-
exports.REJECTED_INCONCLUSIVE = 'LND rejected this check, but not
|
|
4632
|
+
exports.REJECTED_INCONCLUSIVE = 'LND rejected this check, but did not report that the root key ID is gone or that the credential expired. A caveat that depends on the caller (an IP lock, or a custom caveat such as a canary\'s) can make LND accept it from another address or while a middleware runs. Treat it as live until its root key ID is revoked.';
|
|
4633
|
+
exports.LOCAL_EXPIRY_UNCONFIRMED = "By this machine's clock a time-before caveat has passed, but LND did not report the expiry: the clocks may differ, or LND refused an earlier caveat first. A local clock is no proof that the credential is dead.";
|
|
4634
|
+
exports.SIGNATURE_MISMATCH_INCONCLUSIVE = 'LND reports a signature mismatch, but the root key ID is still listed (or could not be listed). This copy is not the credential LND issued: either the root keys were regenerated, or this copy was altered (a flipped byte, a mangled paste, a decoy) and the original it came from may still be accepted. Treat the original as live until its root key ID is revoked.';
|
|
4635
|
+
exports.SIGNATURE_MISMATCH_HOW_TO_CONCLUDE = 'For a conclusive answer, check a copy whose bytes are known to be unaltered: macwarden verify answers conclusively for the exact file it recorded in the registry (same fingerprint). Otherwise compare the fingerprint of this file (above) with one recorded before the incident, such as an inventory --report export or a backup: if they are equal, the bytes are unaltered and the mismatch means the root key was regenerated.';
|
|
4636
|
+
exports.SIGNATURE_MISMATCH_ROOT_KEY_0 = 'On root key ID 0 nothing can be revoked on its own: if you have not regenerated every root key (lncli changepassword --new_mac_root_key, which macwarden never runs), assume the original is still live.';
|
|
4637
|
+
exports.OWN_ROOT_KEY_INCONCLUSIVE = "This credential is on the root key ID of the credential macwarden is running with, which macwarden revoke refuses. To invalidate it: bake a new session credential on its own root key ID, switch MACWARDEN_MACAROON (or --macaroon) to it, then revoke this root key ID from the new session with macwarden revoke <id> --unregistered (or lncli deletemacaroonid <id>). That also invalidates the old session credential.";
|
|
4638
|
+
exports.ROOT_KEY_0_INCONCLUSIVE = 'It is on root key ID 0, which LND cannot revoke on its own: if it may be live, only regenerating every root key invalidates it (lncli changepassword --new_mac_root_key, which macwarden never runs; see docs/incident-playbook.md section 5).';
|
|
4247
4639
|
exports.CUSTOM_CAVEAT_REJECTED = 'It carries a custom caveat (a canary, or another lnd-custom caveat): LND accepts it only while a middleware for that caveat, such as macwarden watch, is registered.';
|
|
4248
4640
|
function fail(code, step, message, warnings = []) {
|
|
4249
4641
|
return { schema: 1, command: 'verify', ok: false, warnings, result: null, error: { code, step, message } };
|
|
@@ -4291,9 +4683,9 @@ function parse(args, env) {
|
|
|
4291
4683
|
}
|
|
4292
4684
|
if (inputs.length !== 1)
|
|
4293
4685
|
return 'expected one input: a credential file, or - for stdin';
|
|
4294
|
-
const host = supplied.get(
|
|
4295
|
-
|
|
4296
|
-
|
|
4686
|
+
const { host, cert, own, problem: mixedSource } = (0, online_js_1.connectionSettings)(flag => supplied.get(flag), env);
|
|
4687
|
+
if (mixedSource)
|
|
4688
|
+
return mixedSource;
|
|
4297
4689
|
const connection = (0, online_js_1.connectionProblem)(host, cert, own);
|
|
4298
4690
|
if (connection)
|
|
4299
4691
|
return connection;
|
|
@@ -4317,7 +4709,7 @@ function registryMatch(entries, pubkey, facts) {
|
|
|
4317
4709
|
const related = pick(local.filter(sameId));
|
|
4318
4710
|
return related ? { label: related.label, status: related.status, match: 'root-key-id' } : null;
|
|
4319
4711
|
}
|
|
4320
|
-
function guidance(result) {
|
|
4712
|
+
function guidance(result, localExpiry) {
|
|
4321
4713
|
const id = result.rootKeyId;
|
|
4322
4714
|
if (result.status === 'accepted') {
|
|
4323
4715
|
const lines = ['LND accepts this credential now: whoever holds a copy can use it against this node.'];
|
|
@@ -4338,15 +4730,31 @@ function guidance(result) {
|
|
|
4338
4730
|
return lines;
|
|
4339
4731
|
}
|
|
4340
4732
|
if (!result.conclusive) {
|
|
4341
|
-
const lines = [exports.REJECTED_INCONCLUSIVE];
|
|
4733
|
+
const lines = [result.rejection === 'signature-mismatch' ? exports.SIGNATURE_MISMATCH_INCONCLUSIVE : exports.REJECTED_INCONCLUSIVE];
|
|
4734
|
+
if (result.rejection === 'signature-mismatch')
|
|
4735
|
+
lines.push(exports.SIGNATURE_MISMATCH_HOW_TO_CONCLUDE);
|
|
4736
|
+
if (result.rejection === 'signature-mismatch' && id === '0')
|
|
4737
|
+
lines.push(exports.SIGNATURE_MISMATCH_ROOT_KEY_0);
|
|
4342
4738
|
if (result.caveats.some(caveat => caveat.type === 'lnd-custom'))
|
|
4343
4739
|
lines.push(exports.CUSTOM_CAVEAT_REJECTED);
|
|
4344
|
-
if (
|
|
4740
|
+
if (localExpiry)
|
|
4741
|
+
lines.push(exports.LOCAL_EXPIRY_UNCONFIRMED);
|
|
4742
|
+
// Never a revoke command macwarden would refuse (RL-06): not for root key ID 0 (SAFE-03),
|
|
4743
|
+
// not for macwarden's own root key ID (revoke V4).
|
|
4744
|
+
if (id === '0') {
|
|
4745
|
+
if (result.rejection !== 'signature-mismatch')
|
|
4746
|
+
lines.push(exports.ROOT_KEY_0_INCONCLUSIVE);
|
|
4747
|
+
}
|
|
4748
|
+
else if (result.ownCredential)
|
|
4749
|
+
lines.push(exports.OWN_ROOT_KEY_INCONCLUSIVE);
|
|
4750
|
+
else
|
|
4345
4751
|
lines.push(`To make sure: macwarden revoke ${id}${result.registry === null ? ' --unregistered' : ''}.`);
|
|
4346
4752
|
return lines;
|
|
4347
4753
|
}
|
|
4348
4754
|
const why = result.rejection === 'root-key-missing' ? 'its root key ID no longer exists'
|
|
4349
|
-
: result.rejection === 'signature-mismatch' ?
|
|
4755
|
+
: result.rejection === 'signature-mismatch' ? (result.rootKeyListed === false
|
|
4756
|
+
? 'its signature does not match and its root key ID is no longer listed'
|
|
4757
|
+
: `its root key was regenerated: these are the exact bytes macwarden recorded for root key ID ${id}, and LND no longer accepts their signature`)
|
|
4350
4758
|
: result.rejection === 'expired' ? 'a time-before caveat has passed' : 'its root key ID is no longer listed';
|
|
4351
4759
|
const lines = [`LND rejects this credential now: ${why}. It can no longer be used against this node.`];
|
|
4352
4760
|
if (result.rejection === 'expired' && result.rootKeyListed !== false && id !== '0')
|
|
@@ -4420,7 +4828,7 @@ async function runSteps(args, deps, warnings) {
|
|
|
4420
4828
|
try {
|
|
4421
4829
|
if (request.registry && (0, protected_path_js_1.isProtectedPath)(request.registry))
|
|
4422
4830
|
return fail(3, 'C5', 'protected registry path', warnings);
|
|
4423
|
-
entries = (await (0,
|
|
4831
|
+
entries = (await (0, index_js_5.openRegistry)(request.registry, { fs: deps.fs, env: deps.env })).entries;
|
|
4424
4832
|
}
|
|
4425
4833
|
catch {
|
|
4426
4834
|
return fail(3, 'C5', 'unreadable or invalid registry', warnings);
|
|
@@ -4461,22 +4869,38 @@ async function runSteps(args, deps, warnings) {
|
|
|
4461
4869
|
catch (error) {
|
|
4462
4870
|
return lndFailure(error, 'C7', warnings);
|
|
4463
4871
|
}
|
|
4464
|
-
|
|
4872
|
+
// Only node-side evidence makes a rejection conclusive: LND's own expiry error naming one of
|
|
4873
|
+
// this credential's time-before caveats (judged by the node's clock), or a missing root key ID
|
|
4874
|
+
// (reported by LND or absent from ListMacaroonIDs). The local clock never does (MW110-02): it can be skewed, and an earlier
|
|
4875
|
+
// caveat that depends on the caller (IP lock, custom caveat) may be what LND refused.
|
|
4876
|
+
const nodeExpired = !checked.valid && checked.reason === 'expired'
|
|
4877
|
+
&& facts.caveats.some(caveat => caveat.type === 'time-before' && caveat.value === checked.caveat);
|
|
4465
4878
|
const rejection = checked.valid ? null
|
|
4466
|
-
: checked.reason === '
|
|
4467
|
-
// A rejection holds everywhere when the root key is gone or
|
|
4468
|
-
//
|
|
4469
|
-
|
|
4879
|
+
: checked.reason === 'expired' ? (nodeExpired ? 'expired' : 'other') : checked.reason;
|
|
4880
|
+
// A rejection holds everywhere when the root key ID is gone or the credential expired; any other
|
|
4881
|
+
// rejection may depend on the caller (IP lock, custom caveat) or on the copy itself.
|
|
4882
|
+
// A signature mismatch is what LND answers both after a root key regeneration and for any
|
|
4883
|
+
// altered copy (RT-01): a regeneration keeps every root key ID listed, so a mismatch alone never
|
|
4884
|
+
// proves the original dead. It is conclusive once the root key ID is no longer listed, or when
|
|
4885
|
+
// these are the exact bytes macwarden recorded for this root key ID on this node (registry
|
|
4886
|
+
// fingerprint match): known-genuine bytes can only mismatch because the root key changed.
|
|
4887
|
+
// The own credential is no such evidence: it authenticating proves only that its own bytes are
|
|
4888
|
+
// valid, not whether the target's differ by alteration or by a regeneration since it was baked.
|
|
4889
|
+
const registry = registryMatch(entries, info.pubkey, facts);
|
|
4890
|
+
const recordedBytes = registry?.match === 'fingerprint';
|
|
4891
|
+
const conclusive = checked.valid || rejection === 'root-key-missing' || rejection === 'expired'
|
|
4892
|
+
|| rootKeyListed === false || (rejection === 'signature-mismatch' && recordedBytes);
|
|
4893
|
+
const localExpiry = offline.caveats.some(caveat => caveat.type === 'time-before' && caveat.expired === true);
|
|
4470
4894
|
const partial = {
|
|
4471
4895
|
...offline,
|
|
4472
4896
|
node: { pubkey: info.pubkey, alias: info.alias, network: info.network },
|
|
4473
4897
|
status: checked.valid ? 'accepted' : 'rejected',
|
|
4474
4898
|
rejection, conclusive, rootKeyListed,
|
|
4475
4899
|
ownCredential: facts.rootKeyId === ownFacts.rootKeyId,
|
|
4476
|
-
canBake: offline.grade
|
|
4477
|
-
registry
|
|
4900
|
+
canBake: (0, index_js_3.canBake)(facts.permissions, offline.grade, offline.warnings),
|
|
4901
|
+
registry,
|
|
4478
4902
|
};
|
|
4479
|
-
const result = { ...partial, guidance: guidance(partial) };
|
|
4903
|
+
const result = { ...partial, guidance: guidance(partial, localExpiry) };
|
|
4480
4904
|
// Exit 1 is a finding, as with scan --fail-on: the credential is live, or may be.
|
|
4481
4905
|
return { schema: 1, command: 'verify', ok: true, warnings, result, error: null,
|
|
4482
4906
|
...(result.status === 'accepted' || !result.conclusive ? { exitCode: 1 } : {}) };
|
|
@@ -4493,7 +4917,7 @@ async function run(args, deps) {
|
|
|
4493
4917
|
}
|
|
4494
4918
|
}
|
|
4495
4919
|
async function verify(args) {
|
|
4496
|
-
return run(args, { createAdapter:
|
|
4920
|
+
return run(args, { createAdapter: index_js_4.createLndAdapter, fs: nodeFs, env: { ...process.env },
|
|
4497
4921
|
clock: () => new Date(), stdin: process.stdin });
|
|
4498
4922
|
}
|
|
4499
4923
|
|
|
@@ -4557,6 +4981,7 @@ const protected_path_js_1 = require("../protected-path.js");
|
|
|
4557
4981
|
const bounded_file_js_1 = require("../bounded-file.js");
|
|
4558
4982
|
const index_js_3 = require("../registry/index.js");
|
|
4559
4983
|
const process_domain_js_1 = require("../registry/process-domain.js");
|
|
4984
|
+
const root_key_id_js_1 = require("../root-key-id.js");
|
|
4560
4985
|
const index_js_4 = require("../output/index.js");
|
|
4561
4986
|
const canary_js_1 = require("../watch/canary.js");
|
|
4562
4987
|
const usage_js_1 = require("../watch/usage.js");
|
|
@@ -4632,9 +5057,9 @@ function parse(args, env) {
|
|
|
4632
5057
|
return 'unknown flag';
|
|
4633
5058
|
}
|
|
4634
5059
|
const get = (flag) => { const value = flags.get(flag); return typeof value === 'string' ? value : undefined; };
|
|
4635
|
-
const host =
|
|
4636
|
-
|
|
4637
|
-
|
|
5060
|
+
const { host, cert, own, grpc: grpcValue, problem: mixedSource } = (0, online_js_1.connectionSettings)(get, env, true);
|
|
5061
|
+
if (mixedSource)
|
|
5062
|
+
return mixedSource;
|
|
4638
5063
|
if (!host)
|
|
4639
5064
|
return 'missing --lnd-rest (or MACWARDEN_LND_REST)';
|
|
4640
5065
|
if (!cert)
|
|
@@ -4646,9 +5071,8 @@ function parse(args, env) {
|
|
|
4646
5071
|
const rest = hostPort(host);
|
|
4647
5072
|
if (!rest)
|
|
4648
5073
|
return 'invalid LND REST endpoint';
|
|
4649
|
-
const grpcValue = get('--lnd-grpc') ?? env.MACWARDEN_LND_GRPC;
|
|
4650
5074
|
const grpcSource = get('--lnd-grpc') !== undefined ? '--lnd-grpc'
|
|
4651
|
-
:
|
|
5075
|
+
: grpcValue !== undefined ? 'MACWARDEN_LND_GRPC' : 'the default port 10009 on the --lnd-rest host';
|
|
4652
5076
|
const grpc = grpcValue === undefined ? { host: rest.host, port: 10009 } : hostPort(grpcValue);
|
|
4653
5077
|
if (!grpc)
|
|
4654
5078
|
return 'invalid LND gRPC endpoint';
|
|
@@ -4753,7 +5177,7 @@ async function takeOverStaleLock(path, stale, pid, deps) {
|
|
|
4753
5177
|
}
|
|
4754
5178
|
let moved = '';
|
|
4755
5179
|
try {
|
|
4756
|
-
moved = await
|
|
5180
|
+
moved = await (0, bounded_file_js_1.readPlainFile)(quarantine, deps.fs, bounded_file_js_1.LOCK_FILE_LIMIT);
|
|
4757
5181
|
}
|
|
4758
5182
|
catch { /* treated as not ours */ }
|
|
4759
5183
|
if (moved === stale) {
|
|
@@ -4797,7 +5221,7 @@ async function acquireLock(path, deps) {
|
|
|
4797
5221
|
try {
|
|
4798
5222
|
const current = await deps.fs.lstat(path, { bigint: true });
|
|
4799
5223
|
if (current.dev === identity.dev && current.ino === identity.ino
|
|
4800
|
-
&& await
|
|
5224
|
+
&& await (0, bounded_file_js_1.readPlainFile)(path, deps.fs, bounded_file_js_1.LOCK_FILE_LIMIT) === body)
|
|
4801
5225
|
await deps.fs.unlink(path);
|
|
4802
5226
|
}
|
|
4803
5227
|
catch { /* already gone */ }
|
|
@@ -4809,9 +5233,9 @@ async function acquireLock(path, deps) {
|
|
|
4809
5233
|
let owner = {};
|
|
4810
5234
|
let stale;
|
|
4811
5235
|
try {
|
|
4812
|
-
// The owner record is read only as a plain file: never through a symlink (SAFE-02)
|
|
4813
|
-
|
|
4814
|
-
stale = await
|
|
5236
|
+
// The owner record is read only as a plain file: never through a symlink (SAFE-02),
|
|
5237
|
+
// through one descriptor bound to the checked file and at most LOCK_FILE_LIMIT + 1 bytes.
|
|
5238
|
+
stale = await (0, bounded_file_js_1.readPlainFile)(path, deps.fs, bounded_file_js_1.LOCK_FILE_LIMIT);
|
|
4815
5239
|
owner = JSON.parse(stale);
|
|
4816
5240
|
}
|
|
4817
5241
|
catch {
|
|
@@ -4848,7 +5272,6 @@ async function alertCommandProblem(command, deps) {
|
|
|
4848
5272
|
return null;
|
|
4849
5273
|
}
|
|
4850
5274
|
const CREDENTIAL_ID = /^id:[0-9a-f]{16}$/;
|
|
4851
|
-
const ROOT_KEY_ID = /^(?:0|[1-9][0-9]{0,19})$/;
|
|
4852
5275
|
function labels(entries, pubkey) {
|
|
4853
5276
|
const byRootKey = new Map();
|
|
4854
5277
|
for (const entry of entries) {
|
|
@@ -5114,7 +5537,7 @@ async function runSteps(args, deps) {
|
|
|
5114
5537
|
}
|
|
5115
5538
|
requestKeys.delete(id);
|
|
5116
5539
|
counters.errors += 1;
|
|
5117
|
-
if (CREDENTIAL_ID.test(key.credential) &&
|
|
5540
|
+
if (CREDENTIAL_ID.test(key.credential) && (0, root_key_id_js_1.isRootKeyId)(key.rootKeyId))
|
|
5118
5541
|
usage.record(pubkey, key.credential, key.rootKeyId, key.method, at, true);
|
|
5119
5542
|
emit({ event: 'error', time: at.toISOString(), method: key.method, rootKeyId: key.rootKeyId,
|
|
5120
5543
|
credential: key.credential, label: labelFor(key.rootKeyId) });
|
|
@@ -5141,7 +5564,7 @@ async function runSteps(args, deps) {
|
|
|
5141
5564
|
counters.calls += 1;
|
|
5142
5565
|
if (counters.credentials.size < CREDENTIAL_COUNT_LIMIT)
|
|
5143
5566
|
counters.credentials.add(credential);
|
|
5144
|
-
if (CREDENTIAL_ID.test(credential) &&
|
|
5567
|
+
if (CREDENTIAL_ID.test(credential) && (0, root_key_id_js_1.isRootKeyId)(rootKeyId))
|
|
5145
5568
|
usage.record(pubkey, credential, rootKeyId, call.method, at);
|
|
5146
5569
|
emit({ event: 'call', time: at.toISOString(), method: call.method, rootKeyId, credential,
|
|
5147
5570
|
label: labelFor(rootKeyId) });
|
|
@@ -5530,6 +5953,75 @@ function literalHexSpans(text) {
|
|
|
5530
5953
|
hexSpans(value, starts, ends, found);
|
|
5531
5954
|
return found;
|
|
5532
5955
|
}
|
|
5956
|
+
// Raw bytes rendered as UTF-8 (Buffer#toString('utf8'), TextDecoder, Go's string printing)
|
|
5957
|
+
// keep every byte below 0x80 and every valid sequence, but replace each ill-formed sequence
|
|
5958
|
+
// of one to three bytes with U+FFFD. Lengths and offsets of a binary credential are lost, so
|
|
5959
|
+
// it is recognised by the framing that survives instead. Every such rendering of a credential
|
|
5960
|
+
// the structural decoder accepts starts with U+0002 and a header packet kind (1 or 2; a
|
|
5961
|
+
// non-minimal varint kind renders as U+FFFD... U+0000), and holds, after an end-of-section
|
|
5962
|
+
// U+0000, its signature packet header (kind 6, length 32, each minimal or as U+FFFD... U+0000)
|
|
5963
|
+
// followed by 32 bytes. The span runs from the first such start to the end of the last
|
|
5964
|
+
// signature that can follow it, counting each U+FFFD as at least one byte: never shorter than
|
|
5965
|
+
// the credential's rendering. Bytes 0x80 and above never render as ASCII, so this is sound;
|
|
5966
|
+
// it can only redact more, and only text that already holds these control characters.
|
|
5967
|
+
const NON_ASCII = /[\u0080-]/;
|
|
5968
|
+
const SIGNATURE_BYTES = 32;
|
|
5969
|
+
const MAX_VARINT = 10;
|
|
5970
|
+
/** End (exclusive) of a small varint at `at` rendered as UTF-8, or -1: `minimal` or U+FFFD{1,9} U+0000. */
|
|
5971
|
+
function renderedVarint(text, at, minimal) {
|
|
5972
|
+
if (text[at] === minimal)
|
|
5973
|
+
return at + 1;
|
|
5974
|
+
let cursor = at;
|
|
5975
|
+
while (cursor < text.length && cursor - at < MAX_VARINT - 1 && text[cursor] === '�')
|
|
5976
|
+
cursor++;
|
|
5977
|
+
return cursor > at && text[cursor] === '\u0000' ? cursor + 1 : -1;
|
|
5978
|
+
}
|
|
5979
|
+
/** Fewest and most UTF-8 bytes the code unit at `at` can stand for in a rendering. */
|
|
5980
|
+
function renderedBytes(text, at) {
|
|
5981
|
+
const unit = text.charCodeAt(at);
|
|
5982
|
+
if (unit < 0x80)
|
|
5983
|
+
return [1, 1];
|
|
5984
|
+
if (unit < 0x800)
|
|
5985
|
+
return [2, 2];
|
|
5986
|
+
if (unit === 0xfffd)
|
|
5987
|
+
return [1, 3];
|
|
5988
|
+
if (unit >= 0xdc00 && unit <= 0xdfff)
|
|
5989
|
+
return [0, 0];
|
|
5990
|
+
return unit >= 0xd800 && unit <= 0xdbff ? [4, 4] : [3, 3];
|
|
5991
|
+
}
|
|
5992
|
+
function utf8RenderingSpans(text, starts, ends, found) {
|
|
5993
|
+
let first = -1;
|
|
5994
|
+
for (let at = text.indexOf('\u0002'); at >= 0 && at + 1 < text.length; at = text.indexOf('\u0002', at + 1)) {
|
|
5995
|
+
if (text[at + 1] === '\u0001' || renderedVarint(text, at + 1, '\u0002') !== -1) {
|
|
5996
|
+
first = at;
|
|
5997
|
+
break;
|
|
5998
|
+
}
|
|
5999
|
+
}
|
|
6000
|
+
if (first < 0)
|
|
6001
|
+
return;
|
|
6002
|
+
// The last signature header after the start whose 32 bytes can still follow it.
|
|
6003
|
+
for (let at = text.length - 1; at > first; at--) {
|
|
6004
|
+
if (text[at] !== '\u0000')
|
|
6005
|
+
continue;
|
|
6006
|
+
const kind = renderedVarint(text, at + 1, '\u0006');
|
|
6007
|
+
const body = kind < 0 ? -1 : renderedVarint(text, kind, ' ');
|
|
6008
|
+
if (body < 0)
|
|
6009
|
+
continue;
|
|
6010
|
+
let fewest = 0, most = 0, end = body;
|
|
6011
|
+
while (end < text.length && fewest < SIGNATURE_BYTES) {
|
|
6012
|
+
const [low, high] = renderedBytes(text, end++);
|
|
6013
|
+
fewest += low;
|
|
6014
|
+
most += high;
|
|
6015
|
+
}
|
|
6016
|
+
if (most < SIGNATURE_BYTES)
|
|
6017
|
+
continue;
|
|
6018
|
+
// Keep a surrogate pair whole.
|
|
6019
|
+
if (end < text.length && renderedBytes(text, end)[1] === 0)
|
|
6020
|
+
end++;
|
|
6021
|
+
found.push([starts[first], ends[end - 1]]);
|
|
6022
|
+
return;
|
|
6023
|
+
}
|
|
6024
|
+
}
|
|
5533
6025
|
function spans(text, source, backslashPairs = true) {
|
|
5534
6026
|
const { value, starts, ends } = normalized(text, true, source, true, backslashPairs);
|
|
5535
6027
|
const found = [];
|
|
@@ -5580,6 +6072,9 @@ function spans(text, source, backslashPairs = true) {
|
|
|
5580
6072
|
found.push([raw.starts[pos], raw.ends[pos + length - 1]]);
|
|
5581
6073
|
pos += length - 1;
|
|
5582
6074
|
}
|
|
6075
|
+
// A UTF-8 rendering differs from the latin1 one only when a character is not ASCII.
|
|
6076
|
+
if (NON_ASCII.test(raw.value))
|
|
6077
|
+
utf8RenderingSpans(raw.value, raw.starts, raw.ends, found);
|
|
5583
6078
|
// UTF-16 byte strings can carry raw credentials with zero-byte padding.
|
|
5584
6079
|
for (const phase of [0, 1])
|
|
5585
6080
|
for (const order of [0, 1]) {
|
|
@@ -6522,9 +7017,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6522
7017
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6523
7018
|
};
|
|
6524
7019
|
exports.__esModule = true;
|
|
7020
|
+
exports.BAKE_MACAROON_URI = void 0;
|
|
6525
7021
|
exports.grade = grade;
|
|
6526
7022
|
exports.gradeFacts = gradeFacts;
|
|
6527
7023
|
exports.describe = describe;
|
|
7024
|
+
exports.canBake = canBake;
|
|
6528
7025
|
const lnd_permissions_json_1 = __importDefault(require("./lnd-permissions.json"));
|
|
6529
7026
|
const classes = ['READ', 'RECEIVE', 'OPERATE', 'SPEND', 'ADMIN'];
|
|
6530
7027
|
const knownEntities = new Set([
|
|
@@ -6616,6 +7113,19 @@ function describe(facts) {
|
|
|
6616
7113
|
warnings.add('root-key-0');
|
|
6617
7114
|
return byteSort(warnings);
|
|
6618
7115
|
}
|
|
7116
|
+
/** The exact URI permission LND accepts for BakeMacaroon, besides macaroon:generate. */
|
|
7117
|
+
exports.BAKE_MACAROON_URI = 'uri:/lnrpc.Lightning/BakeMacaroon';
|
|
7118
|
+
const INCOMPLETE = ['identifier-exceeds-bounds', 'caveats-exceed-bounds', 'unassessed-permission', 'unassessed-uri'];
|
|
7119
|
+
/**
|
|
7120
|
+
* Whether a credential can, or may, bake macaroons (MW110-06). Baking needs macaroon:generate or
|
|
7121
|
+
* the BakeMacaroon URI; the ADMIN grade alone does not imply it (macaroon:write is ADMIN, and
|
|
7122
|
+
* LND refuses BakeMacaroon with it). A decode or assessment that is incomplete (truncated
|
|
7123
|
+
* identifier or caveats, unassessed permissions, grade UNASSESSED) fails closed: it may bake.
|
|
7124
|
+
*/
|
|
7125
|
+
function canBake(permissions, assessedGrade, warnings = []) {
|
|
7126
|
+
return permissions.includes('macaroon:generate') || permissions.includes(exports.BAKE_MACAROON_URI)
|
|
7127
|
+
|| assessedGrade === 'UNASSESSED' || warnings.some(warning => INCOMPLETE.includes(warning));
|
|
7128
|
+
}
|
|
6619
7129
|
|
|
6620
7130
|
},
|
|
6621
7131
|
"grouped-runs.js": (module, exports, require) => {
|
|
@@ -6892,7 +7402,11 @@ exports.__esModule = true;
|
|
|
6892
7402
|
exports.parseLndconnect = parseLndconnect;
|
|
6893
7403
|
// Parse the raw query value so URLSearchParams cannot turn a literal plus into
|
|
6894
7404
|
// a space or silently repair malformed percent escapes.
|
|
6895
|
-
|
|
7405
|
+
/**
|
|
7406
|
+
* `standardBase64`: also accept the macaroon in the standard base64 alphabet (`+`, `/`), as
|
|
7407
|
+
* some generators write it. Scan detection uses it; credential input keeps base64url only.
|
|
7408
|
+
*/
|
|
7409
|
+
function parseLndconnect(value, { standardBase64 = false } = {}) {
|
|
6896
7410
|
try {
|
|
6897
7411
|
const fragmentStart = value.indexOf('#');
|
|
6898
7412
|
const queryStart = value.indexOf('?');
|
|
@@ -6908,7 +7422,9 @@ function parseLndconnect(value) {
|
|
|
6908
7422
|
const uri = new URL(value);
|
|
6909
7423
|
if (uri.protocol.toLowerCase() !== 'lndconnect:' || !uri.hostname)
|
|
6910
7424
|
return null;
|
|
6911
|
-
|
|
7425
|
+
let form = decodeURIComponent(encoded);
|
|
7426
|
+
if (standardBase64 && /^[A-Za-z0-9+/]+={0,2}$/.test(form))
|
|
7427
|
+
form = form.replace(/\+/g, '-').replace(/\//g, '_');
|
|
6912
7428
|
if (!/^[A-Za-z0-9_-]+={0,2}$/.test(form) || form.length % 4 === 1)
|
|
6913
7429
|
return null;
|
|
6914
7430
|
const bare = form.replace(/=+$/, '');
|
|
@@ -6923,6 +7439,36 @@ function parseLndconnect(value) {
|
|
|
6923
7439
|
}
|
|
6924
7440
|
}
|
|
6925
7441
|
|
|
7442
|
+
},
|
|
7443
|
+
"instant.js": (module, exports, require) => {
|
|
7444
|
+
"use strict";
|
|
7445
|
+
// UTC instants as macwarden writes them (Date.prototype.toISOString, or the same without the
|
|
7446
|
+
// fraction), read back from the registry and the watch usage file.
|
|
7447
|
+
exports.__esModule = true;
|
|
7448
|
+
exports.isInstant = isInstant;
|
|
7449
|
+
const SHAPE = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(?:\.\d+)?Z$/;
|
|
7450
|
+
function daysIn(year, month) {
|
|
7451
|
+
if (month === 2)
|
|
7452
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
|
|
7453
|
+
return [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
7454
|
+
}
|
|
7455
|
+
/**
|
|
7456
|
+
* True for `YYYY-MM-DDTHH:MM:SS[.fraction]Z` naming a real instant: a month 1 to 12, a day that
|
|
7457
|
+
* exists in that month, hours 0 to 23, minutes and seconds 0 to 59. The shape alone is not
|
|
7458
|
+
* enough: Date.parse gives NaN for month 99 and silently rolls 30 February or 24:00 into
|
|
7459
|
+
* another day, and either would corrupt time arithmetic on the value.
|
|
7460
|
+
*/
|
|
7461
|
+
function isInstant(value) {
|
|
7462
|
+
if (typeof value !== 'string')
|
|
7463
|
+
return false;
|
|
7464
|
+
const match = SHAPE.exec(value);
|
|
7465
|
+
if (!match)
|
|
7466
|
+
return false;
|
|
7467
|
+
const [year, month, day, hour, minute, second] = match.slice(1).map(Number);
|
|
7468
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= daysIn(year, month)
|
|
7469
|
+
&& hour <= 23 && minute <= 59 && second <= 59 && Number.isFinite(Date.parse(value));
|
|
7470
|
+
}
|
|
7471
|
+
|
|
6926
7472
|
},
|
|
6927
7473
|
"lnd/grpc.js": (module, exports, require) => {
|
|
6928
7474
|
"use strict";
|
|
@@ -7250,6 +7796,13 @@ const MAX_RESPONSE = 1_048_576;
|
|
|
7250
7796
|
const TIMEOUT_MS = 10_000;
|
|
7251
7797
|
const ROOT_MISSING = /root key with id [0-9]+ doesn't exist/i;
|
|
7252
7798
|
const SIGNATURE_MISMATCH = /signature mismatch/i;
|
|
7799
|
+
// LND v0.21.4 CheckMacaroonPermissions (rpcserver.go) returns gRPC InvalidArgument with err.Error() of
|
|
7800
|
+
// macaroons/service.go CheckMacAuth. Its bakery (macaroon-bakery v2.0.1) checks every first-party caveat
|
|
7801
|
+
// in order and reports the first that fails as `caveat %q not satisfied: <reason>` (checkers.go), and
|
|
7802
|
+
// for an expired time-before the reason is `macaroon has expired` (checkers/time.go), judged by the
|
|
7803
|
+
// node's clock. Only that exact whole message is expiry evidence; the caveat value is returned so the
|
|
7804
|
+
// caller can match it against the credential's own decoded caveats.
|
|
7805
|
+
const NODE_EXPIRED = /^caveat "time-before ([^"\\]*)" not satisfied: macaroon has expired$/;
|
|
7253
7806
|
const MAX_ROOT_KEY_ID = (1n << 64n) - 1n;
|
|
7254
7807
|
function ownAuthenticationFailure(response) {
|
|
7255
7808
|
if (response.status === 401 || response.status === 403)
|
|
@@ -7517,6 +8070,9 @@ function createLndAdapter(config) {
|
|
|
7517
8070
|
throw new errors_js_1.LndError(5, stage, 'invalid LND response');
|
|
7518
8071
|
const missing = response.status === 400 && ROOT_MISSING.test(message);
|
|
7519
8072
|
const mismatch = response.status === 400 && SIGNATURE_MISMATCH.test(message);
|
|
8073
|
+
const expired = NODE_EXPIRED.exec(message);
|
|
8074
|
+
if (expired && !missing && !mismatch)
|
|
8075
|
+
return { valid: false, reason: 'expired', caveat: expired[1], message: 'LND rejected macaroon' };
|
|
7520
8076
|
const reason = missing && !mismatch ? 'root-key-missing'
|
|
7521
8077
|
: mismatch && !missing
|
|
7522
8078
|
? 'signature-mismatch' : 'other';
|
|
@@ -7909,6 +8465,7 @@ exports.readBoundedFile = exports.LABEL_RULE = void 0;
|
|
|
7909
8465
|
exports.endpoint = endpoint;
|
|
7910
8466
|
exports.credentialText = credentialText;
|
|
7911
8467
|
exports.connectionProblem = connectionProblem;
|
|
8468
|
+
exports.connectionSettings = connectionSettings;
|
|
7912
8469
|
exports.flagProblem = flagProblem;
|
|
7913
8470
|
exports.nodeInfo = nodeInfo;
|
|
7914
8471
|
exports.nodePubkey = nodePubkey;
|
|
@@ -7966,6 +8523,37 @@ function connectionProblem(host, cert, own) {
|
|
|
7966
8523
|
return '--lnd-rest (or MACWARDEN_LND_REST) must be host:port, without scheme, path, query or user information';
|
|
7967
8524
|
return null;
|
|
7968
8525
|
}
|
|
8526
|
+
const CONNECTION = [
|
|
8527
|
+
{ key: 'host', flag: '--lnd-rest', variable: 'MACWARDEN_LND_REST' },
|
|
8528
|
+
{ key: 'cert', flag: '--tls-cert', variable: 'MACWARDEN_TLS_CERT' },
|
|
8529
|
+
{ key: 'own', flag: '--macaroon', variable: 'MACWARDEN_MACAROON' },
|
|
8530
|
+
{ key: 'grpc', flag: '--lnd-grpc', variable: 'MACWARDEN_LND_GRPC' },
|
|
8531
|
+
];
|
|
8532
|
+
/**
|
|
8533
|
+
* The node address, its TLS certificate and the credential macwarden presents come from one
|
|
8534
|
+
* source: when any of them is given as a flag, all of them must be flags, so a credential
|
|
8535
|
+
* exported for one node is never sent to a node named on the command line (and the reverse).
|
|
8536
|
+
* With no connection flag, all come from the environment. `grpc` (watch) joins the set when
|
|
8537
|
+
* `withGrpc` is true. A mixed invocation sets `problem`, naming the variables, never values.
|
|
8538
|
+
*/
|
|
8539
|
+
function connectionSettings(flag, env, withGrpc = false) {
|
|
8540
|
+
const items = CONNECTION.filter(item => withGrpc || item.key !== 'grpc');
|
|
8541
|
+
const flagged = items.filter(item => flag(item.flag) !== undefined);
|
|
8542
|
+
const settings = { problem: null };
|
|
8543
|
+
if (!flagged.length) {
|
|
8544
|
+
for (const item of items)
|
|
8545
|
+
settings[item.key] = env[item.variable];
|
|
8546
|
+
return settings;
|
|
8547
|
+
}
|
|
8548
|
+
for (const item of items)
|
|
8549
|
+
settings[item.key] = flag(item.flag);
|
|
8550
|
+
const mixed = items.filter(item => flag(item.flag) === undefined && env[item.variable] !== undefined && env[item.variable] !== '');
|
|
8551
|
+
if (mixed.length)
|
|
8552
|
+
settings.problem = `${flagged.map(item => item.flag).join(', ')} given on the command line, so ` +
|
|
8553
|
+
`${mixed.map(item => item.variable).join(', ')} from the environment is not used: connection settings come all from flags ` +
|
|
8554
|
+
`or all from the environment; give ${mixed.map(item => item.flag).join(', ')} too, or unset ${mixed.map(item => item.variable).join(', ')}`;
|
|
8555
|
+
return settings;
|
|
8556
|
+
}
|
|
7969
8557
|
/** Wording for a value flag given twice or without a value. */
|
|
7970
8558
|
function flagProblem(flag, kind) {
|
|
7971
8559
|
return kind === 'repeated' ? `${flag} was given more than once` : `${flag} needs a value`;
|
|
@@ -8042,7 +8630,7 @@ function readOnlineCredential(path, io) {
|
|
|
8042
8630
|
}
|
|
8043
8631
|
function onlineWarnings(result) {
|
|
8044
8632
|
const messages = {
|
|
8045
|
-
'acl-best-effort': "the
|
|
8633
|
+
'acl-best-effort': "the volume stores no Windows permissions (FAT or exFAT): anyone with access to the volume can read the file",
|
|
8046
8634
|
'unsafe-arg': 'literal credentials can appear in shell history and process listings',
|
|
8047
8635
|
'own-root-key-0': 'own credential uses root key ID 0',
|
|
8048
8636
|
'snapshot-drift': 'LND permissions differ from the bundled snapshot',
|
|
@@ -8080,6 +8668,8 @@ const patterns_js_1 = require("../scan/patterns.js");
|
|
|
8080
8668
|
const nwc_match_js_1 = require("../scan/nwc-match.js");
|
|
8081
8669
|
const credential_recognition_js_1 = require("../credential-recognition.js");
|
|
8082
8670
|
const redact_js_1 = require("../scan/redact.js");
|
|
8671
|
+
const online_js_1 = require("../online.js");
|
|
8672
|
+
const node_path_1 = require("node:path");
|
|
8083
8673
|
function rotatePlanResult(plan, next, warnings, repaired) {
|
|
8084
8674
|
return { schema: 1, command: 'rotate', ok: true, warnings, result: plan, error: null, display: { finish: { next, repaired } } };
|
|
8085
8675
|
}
|
|
@@ -8168,16 +8758,22 @@ function jsonRedactor() {
|
|
|
8168
8758
|
return safe;
|
|
8169
8759
|
};
|
|
8170
8760
|
}
|
|
8761
|
+
// eslint-disable-next-line no-control-regex, no-misleading-character-class
|
|
8762
|
+
const UNSAFE_TEXT = /[\x00-\x1f\x7f-\xa0\u034f\u115f\u1160\u1680\u17b4\u17b5\u180b-\u180f\u2000-\u200f\u2028-\u202f\u205f-\u206f\u3000\u3164\ufe00-\ufe0f\uffa0\u{e0000}-\u{e0fff}\p{Cf}]/gu;
|
|
8171
8763
|
function safeText(value) {
|
|
8172
8764
|
// Control characters, the Unicode bidirectional controls that can make a terminal show
|
|
8173
8765
|
// text in a different order than it is stored (Trojan Source), and Unicode whitespace
|
|
8174
8766
|
// other than the plain space are escaped. A CI runner trims Unicode whitespace before it
|
|
8175
8767
|
// looks for a '::' workflow command, so none may reach a line start unescaped. The
|
|
8176
8768
|
// legacy '##[' command marker is recognised anywhere in a line, so it is escaped too.
|
|
8177
|
-
//
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8769
|
+
// Invisible characters are escaped as well, so a name cannot look like a different file:
|
|
8770
|
+
// every format character (Unicode Cf: soft hyphen, zero-width characters, word joiner,
|
|
8771
|
+
// tags), the Hangul fillers, the combining grapheme joiner and the variation selectors.
|
|
8772
|
+
return redact(value).replace(UNSAFE_TEXT, character => {
|
|
8773
|
+
const code = character.codePointAt(0);
|
|
8774
|
+
return code < 0x100 ? `\\x${code.toString(16).padStart(2, '0')}`
|
|
8775
|
+
: code <= 0xffff ? `\\u${code.toString(16).padStart(4, '0')}` : `\\u{${code.toString(16)}}`;
|
|
8776
|
+
}).replace(/##\[/g, '##\\x5b');
|
|
8181
8777
|
}
|
|
8182
8778
|
/**
|
|
8183
8779
|
* Text output is often read by CI runners. No line may start, after spaces, with '::'
|
|
@@ -8194,7 +8790,7 @@ const warningMessages = {
|
|
|
8194
8790
|
'unsafe-arg': 'literal credentials can appear in shell history and process listings',
|
|
8195
8791
|
'own-root-key-0': 'own credential uses root key ID 0',
|
|
8196
8792
|
'snapshot-drift': 'LND method permissions differ from the bundled snapshot',
|
|
8197
|
-
'acl-best-effort': "the
|
|
8793
|
+
'acl-best-effort': "the volume stores no Windows permissions (FAT or exFAT): anyone with access to the volume can read the file",
|
|
8198
8794
|
'usage-unreadable': 'the watch usage file could not be read; last-used data is omitted',
|
|
8199
8795
|
'usage-write-failed': 'the watch usage file could not be written; usage since the last save is lost',
|
|
8200
8796
|
'canary-requires-watch': 'LND accepts this canary only while macwarden watch runs; a running watch picks it up within 60 seconds (or at its next reconnect), then denies and alerts on every use its permissions allow',
|
|
@@ -8214,7 +8810,12 @@ function* scanTextLines(result) {
|
|
|
8214
8810
|
// relative path is printed as ./path; it names the same file.
|
|
8215
8811
|
const path = /^[\s\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]*:/.test(finding.path)
|
|
8216
8812
|
? `./${finding.path}` : finding.path;
|
|
8217
|
-
const
|
|
8813
|
+
const others = finding.alsoAt ? `${finding.alsoAt.count} other path${finding.alsoAt.count === 1 ? '' : 's'}` : '';
|
|
8814
|
+
// A work-tree finding's alsoAt lists the file's other hard-link names: they are named, since
|
|
8815
|
+
// removing the credential from one name leaves it readable under the others.
|
|
8816
|
+
const also = !finding.alsoAt ? '' : finding.commit !== undefined ? ` (same content also added at ${others})`
|
|
8817
|
+
: ` (same file also at ${others}: ${finding.alsoAt.paths.map(safeText).join(', ')}`
|
|
8818
|
+
+ `${finding.alsoAt.count > finding.alsoAt.paths.length ? ', ...' : ''})`;
|
|
8218
8819
|
const location = `${finding.commit === undefined ? '' : `${finding.commit.slice(0, 7)}:`}${path}` +
|
|
8219
8820
|
(finding.line === null ? '' : `:${finding.line}:${finding.column}`);
|
|
8220
8821
|
const counted = finding.kind === 'nwc' ? ' (counted as SPEND)'
|
|
@@ -8352,6 +8953,39 @@ function alignedRows(table) {
|
|
|
8352
8953
|
return table.map(cells => cells.map((cell, column) => column === cells.length - 1 ? cell
|
|
8353
8954
|
: cell + ' '.repeat(widths[column] - [...cell].length)).join(' ').trimEnd());
|
|
8354
8955
|
}
|
|
8956
|
+
/**
|
|
8957
|
+
* The argument `revoke` resolves to this row's credential (RL-05): its label only when that label
|
|
8958
|
+
* names exactly one active entry on the node (as revoke V6 requires), otherwise its root key ID.
|
|
8959
|
+
* Null when revoke refuses the row (root key ID 0, macwarden's own credential).
|
|
8960
|
+
*/
|
|
8961
|
+
function revokeTarget(rows, row) {
|
|
8962
|
+
if (row.rootKeyId === '0' || row.flags.includes('default-root-key') || row.flags.includes('own-credential'))
|
|
8963
|
+
return null;
|
|
8964
|
+
const label = row.entry?.label ?? null;
|
|
8965
|
+
return label !== null && rows.filter(other => other.entry?.status === 'active' && other.entry.label === label).length === 1
|
|
8966
|
+
? label : row.rootKeyId;
|
|
8967
|
+
}
|
|
8968
|
+
/**
|
|
8969
|
+
* The rotate command for this row, or null when rotate would refuse it (RL-05): no label, root
|
|
8970
|
+
* key ID 0, macwarden's own credential, a staged replacement or an original that already has one,
|
|
8971
|
+
* an invalid link, a custom caveat (a canary) or unassessed permissions. `--out` defaults to
|
|
8972
|
+
* `<label>-<YYYY-MM-DD>.macaroon` next to the recorded file (or in the current directory when no
|
|
8973
|
+
* file is recorded); ADMIN adds `--allow-admin`, which rotate requires.
|
|
8974
|
+
*/
|
|
8975
|
+
function rotateCommand(row, now) {
|
|
8976
|
+
const entry = row.entry;
|
|
8977
|
+
if (!entry || entry.status !== 'active' || entry.label === null || row.rootKeyId === '0')
|
|
8978
|
+
return null;
|
|
8979
|
+
if (['own-credential', 'default-root-key', 'replacement', 'pending-replacement', 'invalid-replacement']
|
|
8980
|
+
.some(flag => row.flags.includes(flag)))
|
|
8981
|
+
return null;
|
|
8982
|
+
if (entry.caveats.some(caveat => caveat.type === 'lnd-custom') || entry.grade === 'UNASSESSED')
|
|
8983
|
+
return null;
|
|
8984
|
+
const name = `${entry.label}-${new Date(now).toISOString().slice(0, 10)}.macaroon`;
|
|
8985
|
+
const out = entry.outPath ? (0, node_path_1.join)((0, node_path_1.dirname)(entry.outPath), name) : name;
|
|
8986
|
+
return ['macwarden', 'rotate', entry.label, '--out', out, ...(entry.grade === 'ADMIN' ? ['--allow-admin'] : [])]
|
|
8987
|
+
.map(value => (0, online_js_1.shellQuote)(value)).join(' ');
|
|
8988
|
+
}
|
|
8355
8989
|
function inventoryText(result, now) {
|
|
8356
8990
|
const { node, rootKeys: rows } = result;
|
|
8357
8991
|
const lines = [`node ${safeText(node.pubkey)} (${safeText(node.alias)}) ${safeText(node.network)}`];
|
|
@@ -8387,13 +9021,16 @@ function inventoryText(result, now) {
|
|
|
8387
9021
|
status.push('canary: verifiable only while watch runs');
|
|
8388
9022
|
else if (row.verify !== 'not-checked')
|
|
8389
9023
|
status.push(row.verify);
|
|
8390
|
-
|
|
8391
|
-
if (row.flags.includes('expired'))
|
|
8392
|
-
|
|
9024
|
+
// Every printed command runs as printed with the same connection and --registry flags (RL-05).
|
|
9025
|
+
if (row.flags.includes('expired')) {
|
|
9026
|
+
const target = revokeTarget(rows, row);
|
|
9027
|
+
status.push(target === null ? 'expired' : `expired; revoke with: macwarden revoke ${(0, online_js_1.shellQuote)(target)}`);
|
|
9028
|
+
}
|
|
8393
9029
|
else if (row.flags.includes('expiring') && row.expires) {
|
|
8394
9030
|
const left = Date.parse(row.expires) - now;
|
|
8395
9031
|
const span = left >= 86_400_000 ? `${Math.floor(left / 86_400_000)}d` : `${Math.max(1, Math.ceil(left / 3_600_000))}h`;
|
|
8396
|
-
|
|
9032
|
+
const command = rotateCommand(row, now);
|
|
9033
|
+
status.push(command === null ? `expires in ${span}` : `expires in ${span}; rotate with: ${command}`);
|
|
8397
9034
|
}
|
|
8398
9035
|
if (row.flags.includes('pending-replacement')) {
|
|
8399
9036
|
const replacement = rows.find(candidate => candidate.entry?.status === 'active'
|
|
@@ -8457,6 +9094,9 @@ function formatDuration(seconds) {
|
|
|
8457
9094
|
return `${rest}s`;
|
|
8458
9095
|
}
|
|
8459
9096
|
function observationLine(observation) {
|
|
9097
|
+
// seconds null with a coverage record: the record's times could not be evaluated.
|
|
9098
|
+
if (observation.seconds === null && observation.since !== null)
|
|
9099
|
+
return 'window observation window cannot be computed (invalid watch coverage record); narrowing command withheld';
|
|
8460
9100
|
const span = observation.seconds === null ? 'observed for an unknown time (no watch coverage recorded)'
|
|
8461
9101
|
: `observed for ${formatDuration(observation.seconds)} (${observation.sessions} watch session${observation.sessions === 1 ? '' : 's'} since ${safeText(observation.since ?? '')})`;
|
|
8462
9102
|
if (!observation.short)
|
|
@@ -8482,8 +9122,10 @@ function recommendText(result) {
|
|
|
8482
9122
|
}
|
|
8483
9123
|
function bakeText(result, warnings) {
|
|
8484
9124
|
const aclBestEffort = warnings.some(item => (typeof item === "string" ? item : item.code) === "acl-best-effort");
|
|
8485
|
-
|
|
8486
|
-
|
|
9125
|
+
// Printed only after reserveOut verified the result: mode 0600 read back from the open
|
|
9126
|
+
// descriptor, or the owner-only Windows ACL read back while the exclusive handle was held.
|
|
9127
|
+
const protection = process.platform !== "win32" ? "0600, verified"
|
|
9128
|
+
: aclBestEffort ? "no Windows permissions on this volume" : "owner-only Windows ACL, verified";
|
|
8487
9129
|
const { label, permissions, grade, caveats } = result;
|
|
8488
9130
|
if (!('dryRun' in result))
|
|
8489
9131
|
return [
|
|
@@ -8726,7 +9368,7 @@ function* renderChunks(result, mode) {
|
|
|
8726
9368
|
const lines = batched(format === 'sarif'
|
|
8727
9369
|
? (0, scan_formats_js_1.sarifPieces)(result.result, root, result.display?.toolVersion ?? '0.0.0', jsonRedactor())
|
|
8728
9370
|
: format === 'github'
|
|
8729
|
-
? (0, scan_formats_js_1.githubLines)(result.result, root, safeText, [...scanSummaryLines(result.result)].join(''))
|
|
9371
|
+
? (0, scan_formats_js_1.githubLines)(result.result, root, safeText, [...scanSummaryLines(result.result)].join(''), redact)
|
|
8730
9372
|
: scanTextLines(result.result));
|
|
8731
9373
|
// Text and annotation output both go through the line guard; a github annotation's
|
|
8732
9374
|
// own '::' starts its line on purpose and is produced only by githubLines.
|
|
@@ -8858,22 +9500,46 @@ function findingSeverity(finding) {
|
|
|
8858
9500
|
default: return { level: 'note', score: '3.0' };
|
|
8859
9501
|
}
|
|
8860
9502
|
}
|
|
9503
|
+
// alsoAt on a history finding: other paths that added the content; on a work-tree finding:
|
|
9504
|
+
// the file's other hard-link names.
|
|
9505
|
+
const alsoText = (finding) => finding.commit === undefined ? 'same file also at' : 'same content also added at';
|
|
8861
9506
|
const gradeLabel = (finding) => finding.kind === 'nwc' ? 'SPEND (NWC)'
|
|
8862
9507
|
: finding.kind === 'undecodable' || finding.kind === 'undecodable-candidate' ? 'ADMIN (undecodable)'
|
|
8863
9508
|
: finding.grade ?? 'UNASSESSED';
|
|
8864
|
-
/**
|
|
9509
|
+
/**
|
|
9510
|
+
* A finding's path as SARIF/GitHub expect it: relative to the scan root with forward slashes
|
|
9511
|
+
* when inside it, otherwise absolute. A relative path (scan paths are relative to the working
|
|
9512
|
+
* directory, the scan root) is resolved first, so '../x' is outside the root.
|
|
9513
|
+
*/
|
|
8865
9514
|
function locationOf(path, root) {
|
|
8866
|
-
const
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
9515
|
+
const absolute = (0, node_path_1.resolve)(root, path);
|
|
9516
|
+
const rel = (0, node_path_1.relative)(root, absolute);
|
|
9517
|
+
if (rel === '' || (0, node_path_1.isAbsolute)(rel) || rel === '..' || rel.startsWith(`..${node_path_1.sep}`))
|
|
9518
|
+
return { absolute };
|
|
9519
|
+
return { relative: rel.split(node_path_1.sep).join('/') };
|
|
8870
9520
|
}
|
|
8871
|
-
|
|
9521
|
+
const encodeSegments = (parts) => parts.map(encodeURIComponent).join('/');
|
|
9522
|
+
/**
|
|
9523
|
+
* An absolute path as a file URI (RFC 8089), each segment percent-encoded as UTF-8. A Windows
|
|
9524
|
+
* path (drive letter, UNC, or the \\?\ and \\.\ forms) takes '\' and '/' as separators; in a
|
|
9525
|
+
* POSIX path '\' is part of a name and is encoded.
|
|
9526
|
+
*/
|
|
8872
9527
|
function fileUri(path) {
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
9528
|
+
let local = path;
|
|
9529
|
+
let windows = /^[A-Za-z]:[\\/]/.test(local) || /^[\\/]{2}[^\\/]/.test(local);
|
|
9530
|
+
if (/^[\\/]{2}[?.][\\/]/.test(local)) {
|
|
9531
|
+
local = local.slice(4);
|
|
9532
|
+
if (/^UNC[\\/]/i.test(local))
|
|
9533
|
+
local = `\\\\${local.slice(4)}`;
|
|
9534
|
+
windows = true;
|
|
9535
|
+
}
|
|
9536
|
+
if (!windows)
|
|
9537
|
+
return `file://${encodeSegments(local.split('/'))}`;
|
|
9538
|
+
const parts = local.split(/[\\/]/);
|
|
9539
|
+
// UNC \\server\share\x is file://server/share/x; a drive path keeps its colon.
|
|
9540
|
+
if (parts[0] === '' && parts[1] === '')
|
|
9541
|
+
return `file://${encodeURIComponent(parts[2] ?? '')}/${encodeSegments(parts.slice(3))}`;
|
|
9542
|
+
return `file:///${parts[0]}/${encodeSegments(parts.slice(1))}`;
|
|
8877
9543
|
}
|
|
8878
9544
|
function artifactLocation(path, root) {
|
|
8879
9545
|
const where = locationOf(path, root);
|
|
@@ -8896,7 +9562,11 @@ function* sarifPieces(result, root, version, replacer) {
|
|
|
8896
9562
|
const driver = { name: 'macwarden', version, semanticVersion: version, informationUri: HELP, rules };
|
|
8897
9563
|
const headJson = JSON.stringify(head, replacer);
|
|
8898
9564
|
// Columns are Unicode code points from the line start (SPEC, scan); SARIF defaults to UTF-16 units.
|
|
8899
|
-
|
|
9565
|
+
// %SRCROOT% is declared as the scan root, so every relative result URI resolves to its file.
|
|
9566
|
+
const rootUri = fileUri((0, node_path_1.resolve)(root));
|
|
9567
|
+
const baseIds = { '%SRCROOT%': { uri: rootUri.endsWith('/') ? rootUri : `${rootUri}/` } };
|
|
9568
|
+
yield `${headJson.slice(0, -1)},"runs":[{"tool":${JSON.stringify({ driver }, replacer)},`
|
|
9569
|
+
+ `"originalUriBaseIds":${JSON.stringify(baseIds, replacer)},"columnKind":"unicodeCodePoints","results":[`;
|
|
8900
9570
|
let first = true;
|
|
8901
9571
|
let emitted = 0;
|
|
8902
9572
|
for (const finding of result.findings) {
|
|
@@ -8914,14 +9584,14 @@ function* sarifPieces(result, root, version, replacer) {
|
|
|
8914
9584
|
location.logicalLocations = [{ name: finding.commit, kind: 'commit' }];
|
|
8915
9585
|
const related = finding.alsoAt?.paths.map((path, index) => ({ id: index + 1,
|
|
8916
9586
|
physicalLocation: { artifactLocation: artifactLocation(path, root) },
|
|
8917
|
-
message: { text: 'same content also added here' } }));
|
|
9587
|
+
message: { text: finding.commit === undefined ? 'same file (hard link) also here' : 'same content also added here' } }));
|
|
8918
9588
|
const sarifResult = {
|
|
8919
9589
|
ruleId, ruleIndex, level,
|
|
8920
9590
|
message: { text: `${finding.kind} credential, grade ${gradeLabel(finding)}, ${finding.fingerprint}`
|
|
8921
9591
|
+ (finding.rootKeyId === null ? '' : `, root key ${finding.rootKeyId}`)
|
|
8922
9592
|
+ (finding.commit === undefined ? '' : `, added in commit ${finding.commit}`)
|
|
8923
9593
|
+ (finding.context === 'mcp-config' ? ', configured for an AI agent (MCP server)' : '')
|
|
8924
|
-
+ (finding.alsoAt ? `,
|
|
9594
|
+
+ (finding.alsoAt ? `, ${alsoText(finding)} ${finding.alsoAt.count} other path(s)` : '') },
|
|
8925
9595
|
locations: [location],
|
|
8926
9596
|
...(related?.length ? { relatedLocations: related } : {}),
|
|
8927
9597
|
partialFingerprints: { 'macwardenFingerprint/v1': finding.fingerprint },
|
|
@@ -8952,14 +9622,18 @@ function escapeData(value) {
|
|
|
8952
9622
|
function escapeProperty(value) {
|
|
8953
9623
|
return escapeData(value).replace(/:/g, '%3A').replace(/,/g, '%2C');
|
|
8954
9624
|
}
|
|
8955
|
-
/**
|
|
8956
|
-
|
|
9625
|
+
/**
|
|
9626
|
+
* GitHub Actions annotations, one per finding, then the text summary line. `clean` escapes
|
|
9627
|
+
* the message for display; the file property carries the real path (credentials redacted by
|
|
9628
|
+
* `redact`) with GitHub's own property escaping, so the annotation names the actual file.
|
|
9629
|
+
*/
|
|
9630
|
+
function* githubLines(result, root, clean, summary, redact = text => text) {
|
|
8957
9631
|
for (const finding of result.findings) {
|
|
8958
9632
|
const { level } = findingSeverity(finding);
|
|
8959
9633
|
const command = level === 'error' ? 'error' : level === 'warning' ? 'warning' : 'notice';
|
|
8960
9634
|
const where = locationOf(finding.path, root);
|
|
8961
9635
|
const file = 'relative' in where ? where.relative : where.absolute;
|
|
8962
|
-
const properties = [`file=${escapeProperty(
|
|
9636
|
+
const properties = [`file=${escapeProperty(redact(file))}`];
|
|
8963
9637
|
if (finding.line !== null)
|
|
8964
9638
|
properties.push(`line=${finding.line}`);
|
|
8965
9639
|
if (finding.column !== null)
|
|
@@ -8969,7 +9643,7 @@ function* githubLines(result, root, clean, summary) {
|
|
|
8969
9643
|
+ (finding.rootKeyId === null ? '' : ` root key ${finding.rootKeyId}`)
|
|
8970
9644
|
+ (finding.commit === undefined ? '' : ` in commit ${finding.commit.slice(0, 12)}`)
|
|
8971
9645
|
+ (finding.context === 'mcp-config' ? ' (MCP server config)' : '')
|
|
8972
|
-
+ (finding.alsoAt ? ` (
|
|
9646
|
+
+ (finding.alsoAt ? ` (${alsoText(finding)} ${finding.alsoAt.count} other path(s))` : '');
|
|
8973
9647
|
yield `::${command} ${properties.join(',')}::${escapeData(clean(message))}\n`;
|
|
8974
9648
|
}
|
|
8975
9649
|
yield summary;
|
|
@@ -9002,6 +9676,7 @@ function internalErrorMessage(state) {
|
|
|
9002
9676
|
"use strict";
|
|
9003
9677
|
exports.__esModule = true;
|
|
9004
9678
|
exports.isProtectedPath = isProtectedPath;
|
|
9679
|
+
exports.outputNameProblem = outputNameProblem;
|
|
9005
9680
|
exports.resolveUnprotectedPath = resolveUnprotectedPath;
|
|
9006
9681
|
const node_path_1 = require("node:path");
|
|
9007
9682
|
// SAFE-02 names are refused as complete path components before opening them.
|
|
@@ -9014,6 +9689,38 @@ function isProtectedPath(path) {
|
|
|
9014
9689
|
/^(?:wallet[-_.]?password(?:\..+)?|seeds?(?:\..+)?|tls[-_.]?(?:key|privkey)(?:\..+)?)$/.test(name);
|
|
9015
9690
|
});
|
|
9016
9691
|
}
|
|
9692
|
+
// Names Win32 path normalisation changes or maps to a device. Node opens the literal
|
|
9693
|
+
// \\?\ object while Win32 tools (and other programs) open the normalised name, so a
|
|
9694
|
+
// credential file must never carry one. COM/LPT also match the superscript digits.
|
|
9695
|
+
const WINDOWS_DEVICE = /^(?:con|prn|aux|nul|com[0-9\u00b9\u00b2\u00b3]|lpt[0-9\u00b9\u00b2\u00b3]|conin\$|conout\$)(?:[ .]|$)/i;
|
|
9696
|
+
// eslint-disable-next-line no-control-regex
|
|
9697
|
+
const WINDOWS_INVALID = /[<>:"|?*\u0000-\u001f]/;
|
|
9698
|
+
/**
|
|
9699
|
+
* Why an output path cannot be written as given on Windows, or null. Every component
|
|
9700
|
+
* after the drive or UNC share is checked: a trailing dot or space, a reserved device
|
|
9701
|
+
* name (with or without an extension, any case), ':' (an alternate data stream) or a
|
|
9702
|
+
* character Windows forbids. Device namespace paths (\\.\) are refused. Other
|
|
9703
|
+
* platforms accept these names, so the check applies only on Windows.
|
|
9704
|
+
*/
|
|
9705
|
+
function outputNameProblem(path, platform = process.platform) {
|
|
9706
|
+
if (platform !== 'win32')
|
|
9707
|
+
return null;
|
|
9708
|
+
const absolute = node_path_1.win32.resolve(path);
|
|
9709
|
+
if (/^[\\/]{2}\.[\\/]/.test(absolute))
|
|
9710
|
+
return 'a Windows device namespace path (\\\\.\\) is refused';
|
|
9711
|
+
const root = node_path_1.win32.parse(absolute).root;
|
|
9712
|
+
for (const component of absolute.slice(root.length).split('\\')) {
|
|
9713
|
+
if (!component)
|
|
9714
|
+
continue;
|
|
9715
|
+
if (WINDOWS_INVALID.test(component))
|
|
9716
|
+
return 'a path component contains ":" (an alternate data stream) or a character Windows does not allow in names';
|
|
9717
|
+
if (/[. ]$/.test(component))
|
|
9718
|
+
return 'a path component ends with a dot or a space, which Windows removes';
|
|
9719
|
+
if (WINDOWS_DEVICE.test(component))
|
|
9720
|
+
return 'a path component is a reserved Windows device name (CON, PRN, AUX, NUL, COM1-9, LPT1-9)';
|
|
9721
|
+
}
|
|
9722
|
+
return null;
|
|
9723
|
+
}
|
|
9017
9724
|
async function resolveUnprotectedPath(path, fs) {
|
|
9018
9725
|
if (isProtectedPath(path))
|
|
9019
9726
|
throw new Error('protected path');
|
|
@@ -9093,6 +9800,8 @@ exports.Registry = exports.RegistryUsageError = exports.RegistryInputError = voi
|
|
|
9093
9800
|
exports.openRegistry = openRegistry;
|
|
9094
9801
|
exports.replacementProblem = replacementProblem;
|
|
9095
9802
|
exports.replacementLinkProblems = replacementLinkProblems;
|
|
9803
|
+
exports.recordedFileRelation = recordedFileRelation;
|
|
9804
|
+
exports.isStagedReplacement = isStagedReplacement;
|
|
9096
9805
|
exports.hasCustomCaveat = hasCustomCaveat;
|
|
9097
9806
|
const process_domain_js_1 = require("./process-domain.js");
|
|
9098
9807
|
const nodeFs = __importStar(require("node:fs/promises"));
|
|
@@ -9101,7 +9810,8 @@ const node_path_1 = require("node:path");
|
|
|
9101
9810
|
const node_crypto_1 = require("node:crypto");
|
|
9102
9811
|
const protected_path_js_1 = require("../protected-path.js");
|
|
9103
9812
|
const bounded_file_js_1 = require("../bounded-file.js");
|
|
9104
|
-
const
|
|
9813
|
+
const instant_js_1 = require("../instant.js");
|
|
9814
|
+
const root_key_id_js_1 = require("../root-key-id.js");
|
|
9105
9815
|
const fields = ['nodePubkey', 'label', 'rootKeyId', 'permissions', 'caveats', 'grade',
|
|
9106
9816
|
'fingerprint', 'outPath', 'createdAt', 'verified', 'status', 'revokedAt',
|
|
9107
9817
|
'revokeEvidence', 'replaces', 'replacedBy'];
|
|
@@ -9136,13 +9846,12 @@ function exactFields(value, expected) {
|
|
|
9136
9846
|
const keys = Object.keys(value);
|
|
9137
9847
|
return keys.length === expected.length && keys.every(key => expected.includes(key));
|
|
9138
9848
|
}
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
function
|
|
9144
|
-
return typeof value === 'string' &&
|
|
9145
|
-
&& !Number.isNaN(Date.parse(value));
|
|
9849
|
+
const decimalId = root_key_id_js_1.isRootKeyId;
|
|
9850
|
+
// A real instant, not only its shape: 30 February or 24:00 would be read as another day.
|
|
9851
|
+
const timestamp = instant_js_1.isInstant;
|
|
9852
|
+
/** A recorded expiry duration, as `bake --expires` accepts it (RL-03): rotate S11 reuses it. */
|
|
9853
|
+
function duration(value) {
|
|
9854
|
+
return typeof value === 'string' && /^[1-9][0-9]{0,4}[mhd]$/.test(value);
|
|
9146
9855
|
}
|
|
9147
9856
|
function validEntry(value) {
|
|
9148
9857
|
if (!isRecord(value) || !exactFields(value, fields))
|
|
@@ -9158,7 +9867,7 @@ function validEntry(value) {
|
|
|
9158
9867
|
&& Object.hasOwn(caveat, 'type') && Object.hasOwn(caveat, 'value')
|
|
9159
9868
|
&& typeof caveat.type === 'string' && caveatTypes.has(caveat.type)
|
|
9160
9869
|
&& typeof caveat.value === 'string'
|
|
9161
|
-
&& (caveat.requested === undefined ||
|
|
9870
|
+
&& (caveat.requested === undefined || (caveat.type === 'time-before' && duration(caveat.requested)))))
|
|
9162
9871
|
return false;
|
|
9163
9872
|
return typeof value.grade === 'string' && grades.has(value.grade)
|
|
9164
9873
|
&& ((typeof value.fingerprint === 'string' && /^fp:[0-9a-f]{16}$/.test(value.fingerprint))
|
|
@@ -9228,9 +9937,14 @@ function alive(pid) {
|
|
|
9228
9937
|
return error.code !== 'ESRCH';
|
|
9229
9938
|
}
|
|
9230
9939
|
}
|
|
9940
|
+
const LOCK_TOKEN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
9941
|
+
// `token` (from 1.1.1) is a random per-acquisition nonce that binds release to this owner's
|
|
9942
|
+
// record; records written without it by earlier versions stay valid.
|
|
9231
9943
|
function validLock(value) {
|
|
9232
9944
|
return isRecord(value) && (exactFields(value, ['pid', 'hostname', 'startedAt'])
|
|
9233
|
-
|| exactFields(value, ['pid', 'hostname', 'startedAt', 'processDomain'])
|
|
9945
|
+
|| exactFields(value, ['pid', 'hostname', 'startedAt', 'processDomain'])
|
|
9946
|
+
|| exactFields(value, ['pid', 'hostname', 'startedAt', 'processDomain', 'token']))
|
|
9947
|
+
&& (value.token === undefined || (typeof value.token === 'string' && LOCK_TOKEN.test(value.token)))
|
|
9234
9948
|
&& Number.isSafeInteger(value.pid) && value.pid > 0
|
|
9235
9949
|
&& typeof value.hostname === 'string' && /^[A-Za-z0-9_.-]+$/.test(value.hostname)
|
|
9236
9950
|
&& timestamp(value.startedAt)
|
|
@@ -9240,6 +9954,7 @@ class Registry {
|
|
|
9240
9954
|
fs;
|
|
9241
9955
|
deps;
|
|
9242
9956
|
path;
|
|
9957
|
+
/** The lock this registry holds: its file identity and the exact owner record written. */
|
|
9243
9958
|
ownedLock;
|
|
9244
9959
|
entries;
|
|
9245
9960
|
originalText;
|
|
@@ -9446,11 +10161,12 @@ class Registry {
|
|
|
9446
10161
|
try {
|
|
9447
10162
|
const handle = await this.fs.open(lockPath, 'wx', 0o600);
|
|
9448
10163
|
try {
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
10164
|
+
const record = JSON.stringify({ pid: process.pid, hostname: (this.deps.hostname ?? node_os_1.hostname)(),
|
|
10165
|
+
startedAt: new Date().toISOString(), processDomain, token: (0, node_crypto_1.randomUUID)() });
|
|
10166
|
+
await handle.writeFile(record);
|
|
10167
|
+
const stat = await handle.stat({ bigint: true });
|
|
9452
10168
|
await handle.close();
|
|
9453
|
-
this.ownedLock = { dev: stat.dev, ino: stat.ino };
|
|
10169
|
+
this.ownedLock = { dev: stat.dev, ino: stat.ino, birth: stat.birthtimeNs, record };
|
|
9454
10170
|
}
|
|
9455
10171
|
catch {
|
|
9456
10172
|
await handle.close().catch(() => { });
|
|
@@ -9465,9 +10181,9 @@ class Registry {
|
|
|
9465
10181
|
}
|
|
9466
10182
|
let lock;
|
|
9467
10183
|
try {
|
|
9468
|
-
// The lock is ours to read only as a plain file: never through a symlink (SAFE-02)
|
|
9469
|
-
|
|
9470
|
-
const text = await
|
|
10184
|
+
// The lock is ours to read only as a plain file: never through a symlink (SAFE-02),
|
|
10185
|
+
// through one descriptor bound to the checked file and at most LOCK_FILE_LIMIT + 1 bytes.
|
|
10186
|
+
const text = await (0, bounded_file_js_1.readPlainFile)(lockPath, this.fs, bounded_file_js_1.LOCK_FILE_LIMIT);
|
|
9471
10187
|
lock = JSON.parse(text);
|
|
9472
10188
|
}
|
|
9473
10189
|
catch {
|
|
@@ -9492,9 +10208,10 @@ class Registry {
|
|
|
9492
10208
|
before = await this.fs.lstat(lockPath);
|
|
9493
10209
|
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.ino === 0)
|
|
9494
10210
|
input('registry-lock', 'stale lock ownership ambiguous; stop writers and recover offline');
|
|
9495
|
-
|
|
10211
|
+
const text = await (0, bounded_file_js_1.readPlainFile)(lockPath, this.fs, bounded_file_js_1.LOCK_FILE_LIMIT);
|
|
10212
|
+
if (text !== JSON.stringify(lock)) {
|
|
9496
10213
|
// Whitespace is allowed, but the owner must still be identical.
|
|
9497
|
-
const reread = JSON.parse(
|
|
10214
|
+
const reread = JSON.parse(text);
|
|
9498
10215
|
if (!validLock(reread) || JSON.stringify(reread) !== JSON.stringify(lock))
|
|
9499
10216
|
input('registry-lock', 'registry lock changed; stop writers and recover offline');
|
|
9500
10217
|
}
|
|
@@ -9505,7 +10222,7 @@ class Registry {
|
|
|
9505
10222
|
await this.fs.rename(lockPath, quarantine);
|
|
9506
10223
|
quarantined = true;
|
|
9507
10224
|
const moved = await this.fs.lstat(quarantine);
|
|
9508
|
-
const movedOwner = JSON.parse(await
|
|
10225
|
+
const movedOwner = JSON.parse(await (0, bounded_file_js_1.readPlainFile)(quarantine, this.fs, bounded_file_js_1.LOCK_FILE_LIMIT));
|
|
9509
10226
|
if (moved.dev !== before.dev || moved.ino !== before.ino || !validLock(movedOwner)
|
|
9510
10227
|
|| JSON.stringify(movedOwner) !== JSON.stringify(lock) || (this.deps.pidAlive ?? alive)(lock.pid)) {
|
|
9511
10228
|
// Never overwrite a replacement writer's pathname. Hard-link restoration
|
|
@@ -9585,8 +10302,14 @@ class Registry {
|
|
|
9585
10302
|
return;
|
|
9586
10303
|
try {
|
|
9587
10304
|
const lockPath = `${this.path}.lock`;
|
|
9588
|
-
const stat = await this.fs.lstat(lockPath);
|
|
9589
|
-
|
|
10305
|
+
const stat = await this.fs.lstat(lockPath, { bigint: true });
|
|
10306
|
+
// Birth time (0 where the file system has none) tells most recycled inodes apart too.
|
|
10307
|
+
if (!stat.isFile() || stat.dev !== owned.dev || stat.ino !== owned.ino || stat.birthtimeNs !== owned.birth)
|
|
10308
|
+
input('registry-lock', 'registry lock owner changed; preserved competing lock');
|
|
10309
|
+
// A file system may hand a freed inode number to the next file at once, so identity
|
|
10310
|
+
// alone cannot tell a replaced lock apart: the record, with this acquisition's token,
|
|
10311
|
+
// must be exactly the one written.
|
|
10312
|
+
if (await (0, bounded_file_js_1.readPlainFile)(lockPath, this.fs, bounded_file_js_1.LOCK_FILE_LIMIT) !== owned.record)
|
|
9590
10313
|
input('registry-lock', 'registry lock owner changed; preserved competing lock');
|
|
9591
10314
|
await this.fs.unlink(lockPath);
|
|
9592
10315
|
}
|
|
@@ -9640,10 +10363,34 @@ function replacementProblem(old, next) {
|
|
|
9640
10363
|
return 'its label differs';
|
|
9641
10364
|
if (next.rootKeyId === old.rootKeyId)
|
|
9642
10365
|
return 'it has the same root key ID';
|
|
9643
|
-
if (Date.parse(next.createdAt) < Date.parse(old.createdAt))
|
|
9644
|
-
return 'it was created before the credential it replaces'
|
|
10366
|
+
if (Date.parse(next.createdAt) < Date.parse(old.createdAt) && !rotateWrittenShape(old, next))
|
|
10367
|
+
return 'it was created before the credential it replaces (if macwarden staged it while this machine\'s clock ' +
|
|
10368
|
+
`was behind, abandon it with macwarden revoke ${next.rootKeyId} and stage again)`;
|
|
9645
10369
|
return null;
|
|
9646
10370
|
}
|
|
10371
|
+
/**
|
|
10372
|
+
* True when a replacement recorded as created before `old` is the link 1.1.0's own stage wrote
|
|
10373
|
+
* under a clock behind O's recorded creation time (RL-02): 1.1.0 recorded N.createdAt as the
|
|
10374
|
+
* local clock at S10, so an NTP step back, a resumed VM or O baked while the clock ran ahead made
|
|
10375
|
+
* macwarden's own link look "created before" O, and finish, respond and inventory then refused
|
|
10376
|
+
* it as tampering. Such a link has exactly the shape rotate writes: O's permissions (as a set)
|
|
10377
|
+
* and grade, at most one caveat and only `time-before` (S11), no `replacedBy`, `createdAt` as
|
|
10378
|
+
* `Date.toISOString()` writes it, and a gap no larger than a clock error (24 hours). The time
|
|
10379
|
+
* order is a plausibility check, not a boundary (whoever can edit the registry can edit
|
|
10380
|
+
* createdAt too), and a link of this shape grants no more than O; a link that is older, broader
|
|
10381
|
+
* or carries anything rotate would not write stays tampering. 1.1.1 records N.createdAt as
|
|
10382
|
+
* max(now, O.createdAt), so it no longer writes such links.
|
|
10383
|
+
*/
|
|
10384
|
+
const CLOCK_ERROR_MS = 86_400_000;
|
|
10385
|
+
function rotateWrittenShape(old, next) {
|
|
10386
|
+
const permissions = new Set(old.permissions);
|
|
10387
|
+
return next.permissions.length === permissions.size && next.permissions.every(permission => permissions.has(permission))
|
|
10388
|
+
&& new Set(next.permissions).size === next.permissions.length
|
|
10389
|
+
&& next.grade === old.grade && next.replacedBy === null
|
|
10390
|
+
&& next.caveats.length <= 1 && next.caveats.every(caveat => caveat.type === 'time-before')
|
|
10391
|
+
&& /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(next.createdAt)
|
|
10392
|
+
&& Date.parse(old.createdAt) - Date.parse(next.createdAt) <= CLOCK_ERROR_MS;
|
|
10393
|
+
}
|
|
9647
10394
|
/**
|
|
9648
10395
|
* Every active staged-replacement link that macwarden could not have written. A link is an
|
|
9649
10396
|
* active entry whose `replaces` names an active entry on the same node (a rotation that
|
|
@@ -9676,6 +10423,36 @@ function replacementLinkProblems(entries) {
|
|
|
9676
10423
|
}
|
|
9677
10424
|
return problems;
|
|
9678
10425
|
}
|
|
10426
|
+
/**
|
|
10427
|
+
* How the credential file found at an entry's recorded `outPath` relates to that entry (RL-01).
|
|
10428
|
+
* - `match`: the entry's credential (same root key ID and fingerprint).
|
|
10429
|
+
* - `other-credential`: a credential on another root key ID. The path was reused, for example
|
|
10430
|
+
* when the operator baked or staged a replacement where the app reads its file. It says
|
|
10431
|
+
* nothing about this entry, so the entry is handled as having no usable file (proof by ID).
|
|
10432
|
+
* - `same-root-key`: the entry's root key ID with another fingerprint (another macaroon on that
|
|
10433
|
+
* root key, such as an attenuated copy, or an edited fingerprint).
|
|
10434
|
+
* - `tampered`: the entry's fingerprint on another root key ID. A fingerprint covers the
|
|
10435
|
+
* identifier, which carries the root key ID, so the entry's root key ID or fingerprint was edited.
|
|
10436
|
+
*/
|
|
10437
|
+
function recordedFileRelation(entry, facts) {
|
|
10438
|
+
const sameId = facts.rootKeyId === BigInt(entry.rootKeyId);
|
|
10439
|
+
const sameFingerprint = facts.fingerprint === entry.fingerprint;
|
|
10440
|
+
if (sameId)
|
|
10441
|
+
return sameFingerprint ? 'match' : 'same-root-key';
|
|
10442
|
+
return sameFingerprint ? 'tampered' : 'other-credential';
|
|
10443
|
+
}
|
|
10444
|
+
/**
|
|
10445
|
+
* Whether `entry` is a staged (pending) replacement: an active entry whose `replaces` names an
|
|
10446
|
+
* active entry on the same node (SPEC 6). One definition for rotate, respond and inventory
|
|
10447
|
+
* (RL-07): a `replaces` naming a revoked entry (a finished rotation) or an entry no longer in
|
|
10448
|
+
* the registry (pruned) does not make the entry a replacement, so it is an original that can
|
|
10449
|
+
* be rotated again.
|
|
10450
|
+
*/
|
|
10451
|
+
function isStagedReplacement(entries, entry) {
|
|
10452
|
+
const node = entry.nodePubkey.toLowerCase();
|
|
10453
|
+
return entry.status === 'active' && entry.replaces !== null && entries.some(old => old.status === 'active'
|
|
10454
|
+
&& old.nodePubkey.toLowerCase() === node && old.rootKeyId === entry.replaces);
|
|
10455
|
+
}
|
|
9679
10456
|
/** An lnd-custom caveat (a canary, or any custom caveat) that a rotation cannot reproduce. */
|
|
9680
10457
|
function hasCustomCaveat(entry) {
|
|
9681
10458
|
return entry.caveats.some(caveat => caveat.type === 'lnd-custom');
|
|
@@ -10013,6 +10790,23 @@ function formatAccessReview(review, format) {
|
|
|
10013
10790
|
return lines.join('\n');
|
|
10014
10791
|
}
|
|
10015
10792
|
|
|
10793
|
+
},
|
|
10794
|
+
"root-key-id.js": (module, exports, require) => {
|
|
10795
|
+
"use strict";
|
|
10796
|
+
// Root key IDs are uint64 and always canonical decimal strings in JSON (SPEC, Output).
|
|
10797
|
+
exports.__esModule = true;
|
|
10798
|
+
exports.MAX_ROOT_KEY_ID = void 0;
|
|
10799
|
+
exports.isRootKeyId = isRootKeyId;
|
|
10800
|
+
exports.MAX_ROOT_KEY_ID = 18446744073709551615n;
|
|
10801
|
+
/**
|
|
10802
|
+
* True for a canonical decimal uint64: digits only, no leading zero except "0" itself, at most
|
|
10803
|
+
* 18446744073709551615. The one rule for every root key ID macwarden reads (registry, usage
|
|
10804
|
+
* and canary records, command selectors), so no reader accepts an ID another one refuses.
|
|
10805
|
+
*/
|
|
10806
|
+
function isRootKeyId(value) {
|
|
10807
|
+
return typeof value === 'string' && /^(?:0|[1-9][0-9]{0,19})$/.test(value) && BigInt(value) <= exports.MAX_ROOT_KEY_ID;
|
|
10808
|
+
}
|
|
10809
|
+
|
|
10016
10810
|
},
|
|
10017
10811
|
"scan/agent-configs.js": (module, exports, require) => {
|
|
10018
10812
|
"use strict";
|
|
@@ -10165,11 +10959,15 @@ exports.readBlobs = readBlobs;
|
|
|
10165
10959
|
// make an offline scan open SMB connections. locateRepository() resolves all of them in
|
|
10166
10960
|
// JavaScript first, refuses any that are not on a local file system before touching them,
|
|
10167
10961
|
// and the git processes then run with GIT_DIR and GIT_WORK_TREE pinned to the result.
|
|
10962
|
+
// Every location is also checked against the protected names (SAFE-02) on its supplied path
|
|
10963
|
+
// and resolved target before it is opened, because git would read the same files.
|
|
10168
10964
|
const node_child_process_1 = require("node:child_process");
|
|
10169
10965
|
const fs = __importStar(require("node:fs/promises"));
|
|
10170
10966
|
const node_os_1 = require("node:os");
|
|
10171
10967
|
const node_path_1 = require("node:path");
|
|
10968
|
+
const bounded_file_js_1 = require("../bounded-file.js");
|
|
10172
10969
|
const cancellation_js_1 = require("../cancellation.js");
|
|
10970
|
+
const protected_path_js_1 = require("../protected-path.js");
|
|
10173
10971
|
class GitHistoryError extends Error {
|
|
10174
10972
|
constructor(message) { super(message); this.name = 'GitHistoryError'; }
|
|
10175
10973
|
}
|
|
@@ -10192,6 +10990,7 @@ exports.PINNED_CONFIG = [
|
|
|
10192
10990
|
'log.mailmap=false', 'mailmap.file=', 'mailmap.blob=',
|
|
10193
10991
|
].flatMap(setting => ['-c', setting]);
|
|
10194
10992
|
const NOT_LOCAL = 'git history requires a repository on a local file system';
|
|
10993
|
+
const PROTECTED = 'git history would open a protected file';
|
|
10195
10994
|
const NOT_WORK_TREE = 'path is not inside a git work tree';
|
|
10196
10995
|
/**
|
|
10197
10996
|
* True when `path` (absolute) names a network or device location: on Windows a UNC
|
|
@@ -10214,25 +11013,35 @@ const MAX_LOCATION_FILE = 64 * 1024;
|
|
|
10214
11013
|
function absolute(base, value) {
|
|
10215
11014
|
return (0, node_path_1.isAbsolute)(value) || (process.platform === 'win32' && node_path_1.win32.isAbsolute(value)) ? (0, node_path_1.resolve)(value) : (0, node_path_1.resolve)(base, value);
|
|
10216
11015
|
}
|
|
10217
|
-
/**
|
|
10218
|
-
|
|
11016
|
+
/**
|
|
11017
|
+
* Resolves a path named inside the repository. Refuses non-local ones before any I/O, and
|
|
11018
|
+
* (SAFE-02) one whose supplied path or resolved target has a protected file or directory
|
|
11019
|
+
* component before anything there is opened.
|
|
11020
|
+
*/
|
|
11021
|
+
async function local(io, base, value) {
|
|
10219
11022
|
const path = absolute(base, value);
|
|
10220
11023
|
if (nonLocalPath(path))
|
|
10221
11024
|
throw new GitHistoryError(NOT_LOCAL);
|
|
10222
|
-
|
|
11025
|
+
if ((0, protected_path_js_1.isProtectedPath)(path))
|
|
11026
|
+
throw new GitHistoryError(PROTECTED);
|
|
11027
|
+
const real = await io.realpath(path).catch(() => null);
|
|
10223
11028
|
if (real !== null && nonLocalPath(real))
|
|
10224
11029
|
throw new GitHistoryError(NOT_LOCAL);
|
|
11030
|
+
if (real !== null && (0, protected_path_js_1.isProtectedPath)(real))
|
|
11031
|
+
throw new GitHistoryError(PROTECTED);
|
|
10225
11032
|
return real ?? path;
|
|
10226
11033
|
}
|
|
10227
11034
|
exports.PREFLIGHT_LIMITS = { files: 256, bytes: 1024 * 1024, edges: 4096 };
|
|
10228
11035
|
/** One preflight: budgets, counters, memoized results and the chain currently being walked. */
|
|
10229
11036
|
class Preflight {
|
|
11037
|
+
io;
|
|
10230
11038
|
stats;
|
|
10231
11039
|
limits;
|
|
10232
11040
|
/** Canonical location → its expansion, once validated. Each location is read at most once. */
|
|
10233
11041
|
done = new Map();
|
|
10234
11042
|
walking = new Set();
|
|
10235
|
-
constructor(limits = {}, stats) {
|
|
11043
|
+
constructor(io, limits = {}, stats) {
|
|
11044
|
+
this.io = io;
|
|
10236
11045
|
this.limits = { ...exports.PREFLIGHT_LIMITS, ...limits };
|
|
10237
11046
|
this.stats = Object.assign(stats ?? {}, { files: 0, bytes: 0, edges: 0, expandedEdges: 0, expandedBytes: 0 });
|
|
10238
11047
|
}
|
|
@@ -10246,18 +11055,34 @@ class Preflight {
|
|
|
10246
11055
|
throw new GitHistoryError(UNREADABLE);
|
|
10247
11056
|
}
|
|
10248
11057
|
edge() { this.stats.edges++; this.step(); }
|
|
11058
|
+
/**
|
|
11059
|
+
* Reads one location file, or null when it is absent or not a regular file. A protected
|
|
11060
|
+
* path or resolved target (SAFE-02) is refused before it is opened; the content is read
|
|
11061
|
+
* through one descriptor checked to be the regular file that was resolved, and at most
|
|
11062
|
+
* MAX_LOCATION_FILE + 1 bytes are ever read.
|
|
11063
|
+
*/
|
|
10249
11064
|
async small(path) {
|
|
10250
11065
|
this.stats.files++;
|
|
10251
11066
|
this.step();
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
11067
|
+
if ((0, protected_path_js_1.isProtectedPath)(path))
|
|
11068
|
+
throw new GitHistoryError(PROTECTED);
|
|
11069
|
+
let bytes;
|
|
11070
|
+
try {
|
|
11071
|
+
bytes = await (0, bounded_file_js_1.readBoundedFile)(path, this.io, MAX_LOCATION_FILE);
|
|
11072
|
+
}
|
|
11073
|
+
catch (error) {
|
|
11074
|
+
if (error instanceof bounded_file_js_1.BoundedFileError) {
|
|
11075
|
+
if (error.message === 'protected file')
|
|
11076
|
+
throw new GitHistoryError(PROTECTED);
|
|
11077
|
+
if (error.message === 'not a regular file')
|
|
11078
|
+
return null;
|
|
11079
|
+
throw new GitHistoryError(UNREADABLE);
|
|
11080
|
+
}
|
|
11081
|
+
// Absent or inaccessible: git would not read it either.
|
|
11082
|
+
if (['ENOENT', 'ENOTDIR', 'EACCES', 'EPERM', 'ELOOP'].includes(error.code ?? ''))
|
|
11083
|
+
return null;
|
|
10260
11084
|
throw new GitHistoryError(UNREADABLE);
|
|
11085
|
+
}
|
|
10261
11086
|
this.stats.bytes += bytes.length;
|
|
10262
11087
|
this.step();
|
|
10263
11088
|
return bytes.toString('utf8');
|
|
@@ -10331,7 +11156,7 @@ function checkAlternates(run, objects, depth) {
|
|
|
10331
11156
|
if (!line || line.startsWith('#'))
|
|
10332
11157
|
continue;
|
|
10333
11158
|
run.edge();
|
|
10334
|
-
run.follow(own, await checkAlternates(run, await local(objects, unquoteAlternate(line)), depth + 1));
|
|
11159
|
+
run.follow(own, await checkAlternates(run, await local(run.io, objects, unquoteAlternate(line)), depth + 1));
|
|
10335
11160
|
}
|
|
10336
11161
|
return own;
|
|
10337
11162
|
});
|
|
@@ -10380,7 +11205,7 @@ function checkIncludes(run, file, depth) {
|
|
|
10380
11205
|
run.follow(own, NOTHING);
|
|
10381
11206
|
continue;
|
|
10382
11207
|
}
|
|
10383
|
-
run.follow(own, await checkIncludes(run, await local((0, node_path_1.dirname)(file), named), depth + 1));
|
|
11208
|
+
run.follow(own, await checkIncludes(run, await local(run.io, (0, node_path_1.dirname)(file), named), depth + 1));
|
|
10384
11209
|
}
|
|
10385
11210
|
return own;
|
|
10386
11211
|
});
|
|
@@ -10396,17 +11221,18 @@ function ceilings() {
|
|
|
10396
11221
|
* directory or gitfile in it or a parent) and validates every location git would read.
|
|
10397
11222
|
*/
|
|
10398
11223
|
async function locateRepository(directory, options = {}) {
|
|
10399
|
-
const
|
|
11224
|
+
const io = options.io ?? fs;
|
|
11225
|
+
const run = new Preflight(io, options.limits, options.stats);
|
|
10400
11226
|
if (nonLocalPath((0, node_path_1.resolve)(directory)))
|
|
10401
11227
|
throw new GitHistoryError(NOT_LOCAL);
|
|
10402
|
-
let current = await
|
|
11228
|
+
let current = await io.realpath(directory).catch(() => { throw new GitHistoryError(NOT_WORK_TREE); });
|
|
10403
11229
|
if (nonLocalPath(current))
|
|
10404
11230
|
throw new GitHistoryError(NOT_LOCAL);
|
|
10405
11231
|
const ceiling = ceilings();
|
|
10406
11232
|
const key = (path) => process.platform === 'win32' ? path.toLowerCase() : path;
|
|
10407
11233
|
for (;;) {
|
|
10408
11234
|
const candidate = (0, node_path_1.join)(current, '.git');
|
|
10409
|
-
const stat = await
|
|
11235
|
+
const stat = await io.lstat(candidate).catch(() => null);
|
|
10410
11236
|
if (stat) {
|
|
10411
11237
|
let gitDir;
|
|
10412
11238
|
if (stat.isFile()) {
|
|
@@ -10414,15 +11240,15 @@ async function locateRepository(directory, options = {}) {
|
|
|
10414
11240
|
const target = text === null ? null : /^gitdir:\s*(.+?)\s*$/m.exec(text)?.[1];
|
|
10415
11241
|
if (!target)
|
|
10416
11242
|
throw new GitHistoryError(NOT_WORK_TREE);
|
|
10417
|
-
gitDir = await local(current, target);
|
|
11243
|
+
gitDir = await local(io, current, target);
|
|
10418
11244
|
}
|
|
10419
11245
|
else
|
|
10420
|
-
gitDir = await local(current, candidate);
|
|
10421
|
-
const gitStat = await
|
|
11246
|
+
gitDir = await local(io, current, candidate);
|
|
11247
|
+
const gitStat = await io.stat(gitDir).catch(() => null);
|
|
10422
11248
|
if (!gitStat?.isDirectory())
|
|
10423
11249
|
throw new GitHistoryError(NOT_WORK_TREE);
|
|
10424
11250
|
const common = await run.small((0, node_path_1.join)(gitDir, 'commondir'));
|
|
10425
|
-
const commonDir = common === null ? gitDir : await local(gitDir, common.trim());
|
|
11251
|
+
const commonDir = common === null ? gitDir : await local(io, gitDir, common.trim());
|
|
10426
11252
|
run.root(await checkAlternates(run, (0, node_path_1.join)(commonDir, 'objects'), 0));
|
|
10427
11253
|
run.root(await checkIncludes(run, (0, node_path_1.join)(commonDir, 'config'), 0));
|
|
10428
11254
|
run.root(await checkIncludes(run, (0, node_path_1.join)(gitDir, 'config.worktree'), 0));
|
|
@@ -10858,7 +11684,20 @@ function parseIgnoreFile(content) {
|
|
|
10858
11684
|
const match = /^(fp:[0-9a-fA-F]{16})(?:\s+(\S+))?$/.exec(line);
|
|
10859
11685
|
if (!match)
|
|
10860
11686
|
throw new IgnoreFileError();
|
|
10861
|
-
const
|
|
11687
|
+
const glob = match[2];
|
|
11688
|
+
let matches;
|
|
11689
|
+
if (glob === undefined)
|
|
11690
|
+
matches = () => true;
|
|
11691
|
+
else if (glob.startsWith('/')) {
|
|
11692
|
+
// As in .gitignore, a leading '/' anchors the glob at the ignore file's directory. Globs
|
|
11693
|
+
// already match whole relative paths, so the '/' is dropped for them; an absolute
|
|
11694
|
+
// reported path (--ignore-file) is matched against the glob as written.
|
|
11695
|
+
const anchored = globMatcher(glob.slice(1));
|
|
11696
|
+
const absolute = globMatcher(glob);
|
|
11697
|
+
matches = path => path.startsWith('/') ? absolute(path) : anchored(path);
|
|
11698
|
+
}
|
|
11699
|
+
else
|
|
11700
|
+
matches = globMatcher(glob);
|
|
10862
11701
|
if (entries.length >= 1024)
|
|
10863
11702
|
throw new IgnoreFileError();
|
|
10864
11703
|
entries.push({ fingerprint: match[1].toLowerCase(), matches });
|
|
@@ -10903,7 +11742,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
10903
11742
|
};
|
|
10904
11743
|
})();
|
|
10905
11744
|
exports.__esModule = true;
|
|
10906
|
-
exports.readBounded = exports.SCAN_CONCURRENCY = exports.FindingsLimitError = exports.HISTORY_DEDUP_WORK_LIMIT = exports.ALSO_AT_EXAMPLES = exports.FINDINGS_LIMIT = exports.GitHistoryError = exports.IgnoreFileError = void 0;
|
|
11745
|
+
exports.SCAN_OPEN_FLAGS = exports.readBounded = exports.SCAN_CONCURRENCY = exports.FindingsLimitError = exports.HISTORY_DEDUP_WORK_LIMIT = exports.ALSO_AT_EXAMPLES = exports.FINDINGS_LIMIT = exports.CANDIDATE_LIMIT = exports.GitHistoryError = exports.IgnoreFileError = void 0;
|
|
10907
11746
|
exports.scan = scan;
|
|
10908
11747
|
const mcp_context_js_1 = require("./mcp-context.js");
|
|
10909
11748
|
const node_crypto_1 = require("node:crypto");
|
|
@@ -10952,14 +11791,61 @@ const detectors = [
|
|
|
10952
11791
|
{ kind: 'nwc', pattern: patterns_js_1.nwc },
|
|
10953
11792
|
{ kind: 'macaroon-hex', pattern: patterns_js_1.macaroonHex },
|
|
10954
11793
|
{ kind: 'macaroon-base64', pattern: patterns_js_1.macaroonBase64 },
|
|
11794
|
+
// RR-10, scan only (not part of the published patterns): hex with a separator between every
|
|
11795
|
+
// byte (colon hex, xxd -g1, spaced bytes) is decoded; the prefix of an xxd default dump
|
|
11796
|
+
// (two-byte groups, either alignment) is a candidate, since a dump spans several lines.
|
|
11797
|
+
{ kind: 'macaroon-hex', pattern: /02[: -]01[: -]03[: -]6c[: -]6e[: -]64[: -]02(?:[: -][0-9a-f]{2}){20,}/i },
|
|
11798
|
+
{ kind: 'macaroon-hex', pattern: /0201 036c 6e64 02(?:[0-9a-f]{2})?|02 0103 6c6e 6402/i },
|
|
11799
|
+
];
|
|
11800
|
+
// RR-10: a prefix whose continuation is shorter than the published patterns need and that
|
|
11801
|
+
// ends its line may be a credential wrapped narrower than 49 columns. It is joined with the
|
|
11802
|
+
// following lines of the same alphabet and reported when the joined text decodes.
|
|
11803
|
+
const NARROW_WRAP = [
|
|
11804
|
+
{ kind: 'macaroon-base64', start: /AgEDbG5kA[A-Za-z0-9+/_-]{0,39}\s*$/, more: /^[A-Za-z0-9+/_-]+={0,2}$/ },
|
|
11805
|
+
{ kind: 'macaroon-hex', start: /0201036c6e6402[0-9a-f]{0,39}\s*$/i, more: /^[0-9a-f]+$/i },
|
|
10955
11806
|
];
|
|
10956
11807
|
const detectorRegexes = detectors.map(({ kind, pattern }) => ({ kind, regex: new RegExp(pattern.source, `${pattern.flags.replace('g', '')}g`) }));
|
|
10957
11808
|
// Every detector match contains one of these (case-insensitive superset), so a line
|
|
10958
11809
|
// without any of them cannot produce a hit and is not split into code points.
|
|
10959
11810
|
// "walletconnect%3a" covers percent-encoded NWC URIs, which are matched on a normalised copy.
|
|
10960
|
-
const
|
|
10961
|
-
|
|
10962
|
-
|
|
11811
|
+
const PLAIN_MAY_MATCH = /lndconnect:\/\/|walletconnect:|walletconnect%3a|0201036c6e6402|AgEDbG5kA|02[: -]01[: -]03[: -]6c|0201 036c 6e64 02|02 0103 6c6e 6402/i;
|
|
11812
|
+
/**
|
|
11813
|
+
* The part of base64(prefix) that does not depend on the bytes before or after the prefix,
|
|
11814
|
+
* for each of the three byte alignments: text that holds the base64 of a text credential
|
|
11815
|
+
* (a Kubernetes Secret's data, Helm values) holds one of these.
|
|
11816
|
+
*/
|
|
11817
|
+
function encodedCores(prefix) {
|
|
11818
|
+
return [0, 1, 2].map(lead => {
|
|
11819
|
+
const encode = (fill) => Buffer.concat([Buffer.alloc(lead, fill), Buffer.from(prefix), Buffer.alloc(3, fill)]).toString('base64');
|
|
11820
|
+
const [low, high] = [encode(0), encode(0xff)];
|
|
11821
|
+
let best = '';
|
|
11822
|
+
for (let at = 0, run = 0; at < low.length; at++) {
|
|
11823
|
+
run = low[at] === high[at] ? run + 1 : 0;
|
|
11824
|
+
if (run > best.length)
|
|
11825
|
+
best = low.slice(at - run + 1, at + 1);
|
|
11826
|
+
}
|
|
11827
|
+
return best;
|
|
11828
|
+
});
|
|
11829
|
+
}
|
|
11830
|
+
const ENCODED_PREFIXES = ['0201036c6e6402', '0201036C6E6402', 'AgEDbG5kA', 'lndconnect://'];
|
|
11831
|
+
// Base64 that may hold a text credential. Case matters: it is matched on the encoded text.
|
|
11832
|
+
const ENCODED_CANDIDATE = new RegExp(ENCODED_PREFIXES.flatMap(encodedCores)
|
|
11833
|
+
.map(core => core.replace(/[+/]/g, match => `\\${match}`)).join('|'));
|
|
11834
|
+
// A line that cannot produce a hit, plain or encoded, is not split into code points.
|
|
11835
|
+
const MAY_MATCH = { test: (text) => PLAIN_MAY_MATCH.test(text) || ENCODED_CANDIDATE.test(text) };
|
|
11836
|
+
/**
|
|
11837
|
+
* Every detector match of up to this many code points is matched whole wherever it starts in
|
|
11838
|
+
* a line. It covers the largest supported credential encodings with room to spare: the hex of
|
|
11839
|
+
* a 64 KiB credential (131,072), its base64 or base64url (87,382) with lndconnect host and
|
|
11840
|
+
* certificate parameters, and NWC URIs. A match still cut by a window end is reported as an
|
|
11841
|
+
* undecodable-candidate, never dropped.
|
|
11842
|
+
*/
|
|
11843
|
+
exports.CANDIDATE_LIMIT = 262_144;
|
|
11844
|
+
// Windows start every CANDIDATE_LIMIT code points and are long enough that a match starting
|
|
11845
|
+
// anywhere in the first CANDIDATE_LIMIT code points has CANDIDATE_LIMIT code points of room.
|
|
11846
|
+
// Each code point is matched at most twice, so time and memory stay linear in the line.
|
|
11847
|
+
const STEP = exports.CANDIDATE_LIMIT;
|
|
11848
|
+
const WINDOW = STEP + exports.CANDIDATE_LIMIT - 1;
|
|
10963
11849
|
function fingerprint(data) {
|
|
10964
11850
|
return `fp:${(0, node_crypto_1.createHash)('sha256').update(data).digest('hex').slice(0, 16)}`;
|
|
10965
11851
|
}
|
|
@@ -11014,31 +11900,69 @@ function decodeBase64(value) {
|
|
|
11014
11900
|
return null;
|
|
11015
11901
|
return bytes;
|
|
11016
11902
|
}
|
|
11903
|
+
function decoded(bytes) {
|
|
11904
|
+
try {
|
|
11905
|
+
return (0, index_js_1.decodeMacaroon)(new secret_js_1.Secret(bytes));
|
|
11906
|
+
}
|
|
11907
|
+
catch (error) {
|
|
11908
|
+
if (!(error instanceof index_js_1.DecodeError))
|
|
11909
|
+
throw error;
|
|
11910
|
+
return null;
|
|
11911
|
+
}
|
|
11912
|
+
}
|
|
11017
11913
|
function credentialBytes(hit) {
|
|
11018
11914
|
if (hit.kind === 'macaroon-hex') {
|
|
11019
|
-
|
|
11915
|
+
// Byte separators (RR-10) carry no data.
|
|
11916
|
+
const digits = hit.text.replace(/[: -]/g, '');
|
|
11917
|
+
if (digits.length % 2 !== 0)
|
|
11020
11918
|
return null;
|
|
11021
|
-
return Buffer.from(
|
|
11919
|
+
return Buffer.from(digits, 'hex');
|
|
11022
11920
|
}
|
|
11023
11921
|
if (hit.kind === 'macaroon-base64')
|
|
11024
11922
|
return decodeBase64(hit.text);
|
|
11025
|
-
|
|
11026
|
-
|
|
11027
|
-
|
|
11923
|
+
return null;
|
|
11924
|
+
}
|
|
11925
|
+
/**
|
|
11926
|
+
* Whether a match that ends at its window's end would be longer in the whole line: the same
|
|
11927
|
+
* detector, anchored at the match, matches more of the match followed by the next code point.
|
|
11928
|
+
*/
|
|
11929
|
+
function continues(kind, text, next, regex) {
|
|
11930
|
+
if (kind === 'nwc')
|
|
11931
|
+
return false;
|
|
11932
|
+
const sticky = new RegExp(regex.source, `${regex.flags.replace('g', '')}y`);
|
|
11933
|
+
const longer = sticky.exec(text + next);
|
|
11934
|
+
return longer !== null && longer[0].length > text.length;
|
|
11028
11935
|
}
|
|
11029
11936
|
function collectHits(points, only) {
|
|
11030
11937
|
const hits = new Map();
|
|
11938
|
+
const truncated = new Map();
|
|
11031
11939
|
for (let offset = 0; offset < points.length; offset += STEP) {
|
|
11032
11940
|
const window = points.slice(offset, offset + WINDOW).join('');
|
|
11941
|
+
// The line continues past this window: a match reaching its end may be cut.
|
|
11942
|
+
const cut = offset + WINDOW < points.length ? points[offset + WINDOW] : null;
|
|
11943
|
+
// This window has more room for any match starting in it than the earlier ones had, so
|
|
11944
|
+
// it decides again about every earlier cut match that starts here.
|
|
11945
|
+
for (const [key, hit] of truncated) {
|
|
11946
|
+
if (hit.start >= offset) {
|
|
11947
|
+
hits.delete(key);
|
|
11948
|
+
truncated.delete(key);
|
|
11949
|
+
}
|
|
11950
|
+
}
|
|
11033
11951
|
for (const { kind, regex } of detectorRegexes) {
|
|
11034
|
-
if (only !== undefined && kind
|
|
11952
|
+
if (only !== undefined && !only.includes(kind))
|
|
11035
11953
|
continue;
|
|
11036
11954
|
regex.lastIndex = 0;
|
|
11037
11955
|
let codeUnit = 0;
|
|
11038
11956
|
let codePoint = 0;
|
|
11039
11957
|
// NWC uses a linear matcher equivalent to the published regex (src/scan/nwc-match.ts).
|
|
11040
|
-
|
|
11041
|
-
|
|
11958
|
+
// A prefix whose URI runs to the window end without a secret may be cut by the window.
|
|
11959
|
+
const open = [];
|
|
11960
|
+
const found = kind === 'nwc'
|
|
11961
|
+
? (0, nwc_match_js_1.nwcMatches)(window, cut === null ? undefined : open).map(item => ({ index: item.index, 0: item.text }))
|
|
11962
|
+
: [...window.matchAll(regex)];
|
|
11963
|
+
for (const index of open)
|
|
11964
|
+
found.push({ index, 0: window.slice(index), truncated: true });
|
|
11965
|
+
found.sort((a, b) => a.index - b.index);
|
|
11042
11966
|
for (const match of found) {
|
|
11043
11967
|
// Regex offsets count UTF-16 code units; scan columns count code points.
|
|
11044
11968
|
// Advance once through the window instead of recounting every prefix.
|
|
@@ -11048,26 +11972,238 @@ function collectHits(points, only) {
|
|
|
11048
11972
|
}
|
|
11049
11973
|
const start = offset + codePoint;
|
|
11050
11974
|
const key = `${kind}:${start}`;
|
|
11975
|
+
const cutHere = match.truncated ?? (cut !== null && match.index + match[0].length === window.length &&
|
|
11976
|
+
continues(kind, window.slice(match.index), cut, regex));
|
|
11051
11977
|
const prior = hits.get(key);
|
|
11052
|
-
if (
|
|
11053
|
-
|
|
11978
|
+
if (prior && match[0].length <= prior.text.length)
|
|
11979
|
+
continue;
|
|
11980
|
+
const hit = cutHere ? { kind, start, text: match[0], truncated: true } : { kind, start, text: match[0] };
|
|
11981
|
+
hits.set(key, hit);
|
|
11982
|
+
if (cutHere)
|
|
11983
|
+
truncated.set(key, hit);
|
|
11984
|
+
else
|
|
11985
|
+
truncated.delete(key);
|
|
11054
11986
|
}
|
|
11055
11987
|
}
|
|
11988
|
+
// This window reached the line end: a later one would only rescan its tail.
|
|
11989
|
+
if (cut === null)
|
|
11990
|
+
break;
|
|
11991
|
+
}
|
|
11992
|
+
return [...hits.values()].sort(byStartThenDetector);
|
|
11993
|
+
}
|
|
11994
|
+
/** The line's code points with every "\/" read as "/"; origin and escaped map back to the line. */
|
|
11995
|
+
function slashUnescaped(points) {
|
|
11996
|
+
const out = [];
|
|
11997
|
+
const origin = [];
|
|
11998
|
+
const escaped = [];
|
|
11999
|
+
for (let i = 0; i < points.length; i++) {
|
|
12000
|
+
const pair = points[i] === '\\' && points[i + 1] === '/';
|
|
12001
|
+
out.push(pair ? '/' : points[i]);
|
|
12002
|
+
origin.push(i);
|
|
12003
|
+
escaped.push(pair);
|
|
12004
|
+
if (pair)
|
|
12005
|
+
i++;
|
|
12006
|
+
}
|
|
12007
|
+
return { points: out, origin, escaped };
|
|
12008
|
+
}
|
|
12009
|
+
function byStartThenDetector(a, b) {
|
|
12010
|
+
return a.start - b.start || detectors.findIndex(d => d.kind === a.kind) - detectors.findIndex(d => d.kind === b.kind);
|
|
12011
|
+
}
|
|
12012
|
+
// Punctuation that commonly follows a URI in text (markdown, prose, YAML flow, shell) but is
|
|
12013
|
+
// matched by the lndconnect pattern. It is removed only when the URI as matched does not parse.
|
|
12014
|
+
const TRAILING_PUNCTUATION = /[`)\]}>.,;:!?'"*]+$/;
|
|
12015
|
+
/** The credential an lndconnect match carries, and the match's length in code points once confirmed. */
|
|
12016
|
+
function lndconnectCredential(text) {
|
|
12017
|
+
const bytes = (0, lndconnect_js_1.parseLndconnect)(text, { standardBase64: true });
|
|
12018
|
+
if (bytes)
|
|
12019
|
+
return { bytes, length: Array.from(text).length };
|
|
12020
|
+
const trimmed = text.replace(TRAILING_PUNCTUATION, '');
|
|
12021
|
+
if (trimmed === text)
|
|
12022
|
+
return null;
|
|
12023
|
+
const retried = (0, lndconnect_js_1.parseLndconnect)(trimmed, { standardBase64: true });
|
|
12024
|
+
return retried ? { bytes: retried, length: Array.from(trimmed).length } : null;
|
|
12025
|
+
}
|
|
12026
|
+
/**
|
|
12027
|
+
* The findings of one line (or of one decoded text, see scanEncoded). `column` maps a code
|
|
12028
|
+
* point offset in `text` to the 1-based column reported.
|
|
12029
|
+
*/
|
|
12030
|
+
function scanLine(path, line, text, now, ctx, column) {
|
|
12031
|
+
const findings = [];
|
|
12032
|
+
let dropped = 0;
|
|
12033
|
+
const points = Array.from(text);
|
|
12034
|
+
let hits = collectHits(points);
|
|
12035
|
+
// NWC URIs in common escaped forms (JSON-escaped slashes, percent-encoding, "+" as a
|
|
12036
|
+
// space) are matched on one normalised copy of the line; a URI the line already matched
|
|
12037
|
+
// at the same column is not reported twice (a whole match replaces one cut by a window).
|
|
12038
|
+
const unescaped = (0, nwc_match_js_1.nwcUnescaped)(points, text);
|
|
12039
|
+
if (unescaped) {
|
|
12040
|
+
const seen = new Map(hits.filter(hit => hit.kind === 'nwc').map(hit => [hit.start, hit]));
|
|
12041
|
+
const replaced = new Set();
|
|
12042
|
+
for (const hit of collectHits(unescaped.points, ['nwc'])) {
|
|
12043
|
+
const start = unescaped.origin[hit.start];
|
|
12044
|
+
const prior = seen.get(start);
|
|
12045
|
+
if (prior && !(prior.truncated && !hit.truncated))
|
|
12046
|
+
continue;
|
|
12047
|
+
if (prior)
|
|
12048
|
+
replaced.add(prior);
|
|
12049
|
+
const moved = { ...hit, start };
|
|
12050
|
+
seen.set(start, moved);
|
|
12051
|
+
hits.push(moved);
|
|
12052
|
+
}
|
|
12053
|
+
if (replaced.size)
|
|
12054
|
+
hits = hits.filter(hit => !replaced.has(hit));
|
|
12055
|
+
hits.sort((a, b) => a.start - b.start);
|
|
12056
|
+
}
|
|
12057
|
+
// JSON-escaped slashes ("\/", PHP json_encode by default and some Java and Go encoders) would
|
|
12058
|
+
// stop a base64 macaroon or an lndconnect URI at the backslash: both are also matched on a
|
|
12059
|
+
// copy of the line with "\/" read as "/". A match there replaces the one at the same column
|
|
12060
|
+
// only when it covers more of the line, so an unescaped line yields the same hits.
|
|
12061
|
+
const slashes = text.includes('\\/') ? slashUnescaped(points) : null;
|
|
12062
|
+
if (slashes) {
|
|
12063
|
+
const byKey = new Map(hits.map(hit => [`${hit.kind}:${hit.start}`, hit]));
|
|
12064
|
+
const replaced = new Set();
|
|
12065
|
+
for (const hit of collectHits(slashes.points, ['lndconnect', 'macaroon-base64'])) {
|
|
12066
|
+
const start = slashes.origin[hit.start];
|
|
12067
|
+
const last = hit.start + Array.from(hit.text).length - 1;
|
|
12068
|
+
const extent = slashes.origin[last] + (slashes.escaped[last] ? 2 : 1) - start;
|
|
12069
|
+
const key = `${hit.kind}:${start}`;
|
|
12070
|
+
const prior = byKey.get(key);
|
|
12071
|
+
if (prior && extent <= (prior.extent ?? Array.from(prior.text).length))
|
|
12072
|
+
continue;
|
|
12073
|
+
if (prior)
|
|
12074
|
+
replaced.add(prior);
|
|
12075
|
+
const moved = { ...hit, start, extent };
|
|
12076
|
+
byKey.set(key, moved);
|
|
12077
|
+
hits.push(moved);
|
|
12078
|
+
}
|
|
12079
|
+
if (replaced.size)
|
|
12080
|
+
hits = hits.filter(hit => !replaced.has(hit));
|
|
12081
|
+
hits.sort(byStartThenDetector);
|
|
12082
|
+
}
|
|
12083
|
+
// An lndconnect URI is confirmed first. Only a confirmed URI hides the matches inside it:
|
|
12084
|
+
// a URI that does not parse never hides the credential it carries (fail closed).
|
|
12085
|
+
const lineFindings = [];
|
|
12086
|
+
const spans = [];
|
|
12087
|
+
for (const hit of hits) {
|
|
12088
|
+
if (hit.kind !== 'lndconnect' || hit.truncated)
|
|
12089
|
+
continue;
|
|
12090
|
+
const confirmed = lndconnectCredential(hit.text);
|
|
12091
|
+
const facts = confirmed && decoded(confirmed.bytes);
|
|
12092
|
+
if (!confirmed || !facts) {
|
|
12093
|
+
dropped++;
|
|
12094
|
+
continue;
|
|
12095
|
+
}
|
|
12096
|
+
const length = Array.from(hit.text).length;
|
|
12097
|
+
spans.push({ start: hit.start, end: hit.start + (hit.extent ?? length) - (length - confirmed.length) });
|
|
12098
|
+
lineFindings.push({ hit, finding: withFacts(path, line, column(hit.start), 'lndconnect', facts, now, ctx) });
|
|
11056
12099
|
}
|
|
11057
|
-
const all = [...hits.values()].sort((a, b) => a.start - b.start ||
|
|
11058
|
-
detectors.findIndex(d => d.kind === a.kind) - detectors.findIndex(d => d.kind === b.kind));
|
|
11059
|
-
const spans = all.filter(hit => hit.kind === 'lndconnect').map(hit => ({
|
|
11060
|
-
start: hit.start, end: hit.start + Array.from(hit.text).length,
|
|
11061
|
-
}));
|
|
11062
12100
|
let spanIndex = 0;
|
|
11063
12101
|
let coveredUntil = 0;
|
|
11064
|
-
|
|
12102
|
+
for (const hit of hits) {
|
|
11065
12103
|
while (spanIndex < spans.length && spans[spanIndex].start <= hit.start) {
|
|
11066
12104
|
coveredUntil = Math.max(coveredUntil, spans[spanIndex].end);
|
|
11067
12105
|
spanIndex++;
|
|
11068
12106
|
}
|
|
11069
|
-
|
|
11070
|
-
|
|
12107
|
+
const at = column(hit.start);
|
|
12108
|
+
// A match cut by its window's end may continue past it: report it, never decode a part.
|
|
12109
|
+
if (hit.truncated) {
|
|
12110
|
+
lineFindings.push({ hit, finding: nullFinding(path, line, at, 'undecodable-candidate', fingerprint(hit.text)) });
|
|
12111
|
+
continue;
|
|
12112
|
+
}
|
|
12113
|
+
if (hit.kind === 'lndconnect' || hit.start < coveredUntil)
|
|
12114
|
+
continue;
|
|
12115
|
+
if (hit.kind === 'nwc') {
|
|
12116
|
+
// The matcher ends every match with secret=<64 hex> and at most one delimiter. Any
|
|
12117
|
+
// match is reported (fail closed): "&secret=" and other separators included.
|
|
12118
|
+
const secret = /secret=([0-9a-f]{64})[^0-9a-f]?$/i.exec(hit.text)[1].toLowerCase();
|
|
12119
|
+
ctx.secrets.add(secret);
|
|
12120
|
+
lineFindings.push({ hit, finding: nullFinding(path, line, at, 'nwc', fingerprint(Buffer.from(secret, 'hex'))) });
|
|
12121
|
+
continue;
|
|
12122
|
+
}
|
|
12123
|
+
const bytes = credentialBytes(hit);
|
|
12124
|
+
const facts = bytes && decoded(bytes);
|
|
12125
|
+
lineFindings.push({ hit, finding: facts ? withFacts(path, line, at, hit.kind, facts, now, ctx)
|
|
12126
|
+
: nullFinding(path, line, at, 'undecodable-candidate', fingerprint(hit.text)) });
|
|
12127
|
+
}
|
|
12128
|
+
lineFindings.sort((a, b) => byStartThenDetector(a.hit, b.hit));
|
|
12129
|
+
for (const { finding } of lineFindings)
|
|
12130
|
+
findings.push(finding);
|
|
12131
|
+
return { findings, dropped };
|
|
12132
|
+
}
|
|
12133
|
+
/**
|
|
12134
|
+
* RR-02: credentials stored as the base64 of their text (Kubernetes Secret data, Helm values).
|
|
12135
|
+
* Each base64 run of the line holding an encoded prefix is decoded once at each of the four
|
|
12136
|
+
* character phases, and the decoded text is scanned as a line, one level deep: decoded text
|
|
12137
|
+
* is never decoded again. A finding is reported at the column of its first encoded character.
|
|
12138
|
+
*/
|
|
12139
|
+
function scanEncoded(path, line, text, now, ctx) {
|
|
12140
|
+
const findings = [];
|
|
12141
|
+
const reported = new Set();
|
|
12142
|
+
let dropped = 0;
|
|
12143
|
+
const runs = /[A-Za-z0-9+/]{16,}={0,2}/g;
|
|
12144
|
+
// Columns count code points; base64 is ASCII, so advance a code point counter once.
|
|
12145
|
+
let unit = 0;
|
|
12146
|
+
let point = 0;
|
|
12147
|
+
for (let run = runs.exec(text); run; run = runs.exec(text)) {
|
|
12148
|
+
while (unit < run.index) {
|
|
12149
|
+
unit += text.codePointAt(unit) > 0xffff ? 2 : 1;
|
|
12150
|
+
point++;
|
|
12151
|
+
}
|
|
12152
|
+
if (!ENCODED_CANDIDATE.test(run[0]))
|
|
12153
|
+
continue;
|
|
12154
|
+
const runStart = point;
|
|
12155
|
+
for (let phase = 0; phase < 4 && phase < run[0].length; phase++) {
|
|
12156
|
+
const usable = run[0].slice(phase).replace(/=+$/, '');
|
|
12157
|
+
const decodedText = Buffer.from(usable.slice(0, usable.length - (usable.length % 4 === 1 ? 1 : 0)), 'base64').toString('latin1');
|
|
12158
|
+
if (!PLAIN_MAY_MATCH.test(decodedText))
|
|
12159
|
+
continue;
|
|
12160
|
+
// Decoded byte b starts in encoded character floor(b * 4 / 3) of this phase.
|
|
12161
|
+
const result = scanLine(path, line, decodedText, now, ctx, start => runStart + phase + Math.floor(start * 4 / 3) + 1);
|
|
12162
|
+
dropped += result.dropped;
|
|
12163
|
+
for (const finding of result.findings) {
|
|
12164
|
+
const key = `${finding.column}:${finding.kind}:${finding.fingerprint}`;
|
|
12165
|
+
if (!reported.has(key)) {
|
|
12166
|
+
reported.add(key);
|
|
12167
|
+
findings.push(finding);
|
|
12168
|
+
}
|
|
12169
|
+
}
|
|
12170
|
+
}
|
|
12171
|
+
}
|
|
12172
|
+
return { findings, dropped };
|
|
12173
|
+
}
|
|
12174
|
+
/**
|
|
12175
|
+
* RR-10: a credential wrapped narrower than 49 columns. When line `index` ends with a prefix
|
|
12176
|
+
* too short for the published patterns, the following lines that hold only the same alphabet
|
|
12177
|
+
* (indentation and trailing whitespace aside, at most CANDIDATE_LIMIT characters in all) are
|
|
12178
|
+
* joined to it; the credential is reported only when the joined text decodes. Text after the
|
|
12179
|
+
* credential is ignored, as the decoder ignores trailing bytes.
|
|
12180
|
+
*/
|
|
12181
|
+
function narrowWrap(path, lines, index, found, now, ctx) {
|
|
12182
|
+
let last = index;
|
|
12183
|
+
for (const { kind, start, more } of NARROW_WRAP) {
|
|
12184
|
+
const tail = start.exec(lines[index]);
|
|
12185
|
+
if (!tail)
|
|
12186
|
+
continue;
|
|
12187
|
+
const column = Array.from(lines[index].slice(0, tail.index)).length + 1;
|
|
12188
|
+
if (found.some(finding => finding.column === column))
|
|
12189
|
+
continue;
|
|
12190
|
+
let joined = tail[0].trimEnd();
|
|
12191
|
+
for (let next = index + 1; next < lines.length && joined.length < exports.CANDIDATE_LIMIT; next++) {
|
|
12192
|
+
const part = lines[next].trim();
|
|
12193
|
+
if (!more.test(part))
|
|
12194
|
+
break;
|
|
12195
|
+
joined += part;
|
|
12196
|
+
last = Math.max(last, next);
|
|
12197
|
+
if (part.endsWith('='))
|
|
12198
|
+
break;
|
|
12199
|
+
}
|
|
12200
|
+
const bytes = kind === 'macaroon-hex' ? Buffer.from(joined.slice(0, joined.length - joined.length % 2), 'hex')
|
|
12201
|
+
: Buffer.from(joined.slice(0, exports.CANDIDATE_LIMIT), joined.includes('-') || joined.includes('_') ? 'base64url' : 'base64');
|
|
12202
|
+
const facts = decoded(bytes);
|
|
12203
|
+
if (facts)
|
|
12204
|
+
return { finding: withFacts(path, index + 1, column, kind, facts, now, ctx), last };
|
|
12205
|
+
}
|
|
12206
|
+
return { finding: null, last };
|
|
11071
12207
|
}
|
|
11072
12208
|
function scanText(path, text, now, ctx) {
|
|
11073
12209
|
const findings = [];
|
|
@@ -11075,54 +12211,24 @@ function scanText(path, text, now, ctx) {
|
|
|
11075
12211
|
if (!MAY_MATCH.test(text))
|
|
11076
12212
|
return { findings, dropped };
|
|
11077
12213
|
const lines = text.split(/\r\n|\n|\r/);
|
|
12214
|
+
let joinedUntil = -1;
|
|
11078
12215
|
for (let i = 0; i < lines.length; i++) {
|
|
11079
12216
|
if (!MAY_MATCH.test(lines[i]))
|
|
11080
12217
|
continue;
|
|
11081
|
-
const
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11095
|
-
}
|
|
11096
|
-
hits.sort((a, b) => a.start - b.start);
|
|
11097
|
-
}
|
|
11098
|
-
for (const hit of hits) {
|
|
11099
|
-
const column = hit.start + 1;
|
|
11100
|
-
const line = i + 1;
|
|
11101
|
-
if (hit.kind === 'nwc') {
|
|
11102
|
-
// The matcher ends every match with secret=<64 hex> and at most one delimiter. Any
|
|
11103
|
-
// match is reported (fail closed): "&secret=" and other separators included.
|
|
11104
|
-
const secret = /secret=([0-9a-f]{64})[^0-9a-f]?$/i.exec(hit.text)[1].toLowerCase();
|
|
11105
|
-
ctx.secrets.add(secret);
|
|
11106
|
-
findings.push(nullFinding(path, line, column, 'nwc', fingerprint(Buffer.from(secret, 'hex'))));
|
|
11107
|
-
continue;
|
|
11108
|
-
}
|
|
11109
|
-
const bytes = credentialBytes(hit);
|
|
11110
|
-
if (bytes) {
|
|
11111
|
-
try {
|
|
11112
|
-
const facts = (0, index_js_1.decodeMacaroon)(new secret_js_1.Secret(bytes));
|
|
11113
|
-
findings.push(withFacts(path, line, column, hit.kind, facts, now, ctx));
|
|
11114
|
-
continue;
|
|
11115
|
-
}
|
|
11116
|
-
catch (error) {
|
|
11117
|
-
if (!(error instanceof index_js_1.DecodeError))
|
|
11118
|
-
throw error;
|
|
11119
|
-
}
|
|
11120
|
-
}
|
|
11121
|
-
if (hit.kind === 'lndconnect')
|
|
11122
|
-
dropped++;
|
|
11123
|
-
else
|
|
11124
|
-
findings.push(nullFinding(path, line, column, 'undecodable-candidate', fingerprint(hit.text)));
|
|
11125
|
-
}
|
|
12218
|
+
const line = scanLine(path, i + 1, lines[i], now, ctx, start => start + 1);
|
|
12219
|
+
// Each line is joined into at most one wrap, so joining stays linear in the text.
|
|
12220
|
+
if (i > joinedUntil) {
|
|
12221
|
+
const wrapped = narrowWrap(path, lines, i, line.findings, now, ctx);
|
|
12222
|
+
joinedUntil = wrapped.last;
|
|
12223
|
+
if (wrapped.finding)
|
|
12224
|
+
line.findings.push(wrapped.finding);
|
|
12225
|
+
}
|
|
12226
|
+
const encoded = ENCODED_CANDIDATE.test(lines[i]) ? scanEncoded(path, i + 1, lines[i], now, ctx) : null;
|
|
12227
|
+
const lineFindings = encoded?.findings.length ? [...line.findings, ...encoded.findings].sort((a, b) => a.column - b.column)
|
|
12228
|
+
: line.findings;
|
|
12229
|
+
for (const finding of lineFindings)
|
|
12230
|
+
findings.push(finding);
|
|
12231
|
+
dropped += line.dropped + (encoded?.dropped ?? 0);
|
|
11126
12232
|
}
|
|
11127
12233
|
return { findings, dropped };
|
|
11128
12234
|
}
|
|
@@ -11144,8 +12250,16 @@ function reportedPath(path) {
|
|
|
11144
12250
|
const root = (0, node_path_1.parse)(path).root;
|
|
11145
12251
|
const trailingSeparators = node_path_1.sep === '\\' ? /[\\/]+$/ : /\/+$/;
|
|
11146
12252
|
const trimmed = path.length > root.length ? path.replace(trailingSeparators, '') : path;
|
|
11147
|
-
|
|
12253
|
+
// Only Windows uses '\' as a separator; elsewhere it is an ordinary file name character.
|
|
12254
|
+
return node_path_1.sep === '\\' ? trimmed.replace(/\\/g, '/') : trimmed;
|
|
11148
12255
|
}
|
|
12256
|
+
// Windows and macOS file systems compare names case-insensitively by default, so '.GIT' there
|
|
12257
|
+
// is the '.git' directory. Elsewhere names are case-sensitive and '.GIT' is an ordinary directory.
|
|
12258
|
+
const CASE_INSENSITIVE_NAMES = process.platform === 'win32' || process.platform === 'darwin';
|
|
12259
|
+
function namedAs(name, names) {
|
|
12260
|
+
return names.includes(CASE_INSENSITIVE_NAMES ? name.toLowerCase() : name);
|
|
12261
|
+
}
|
|
12262
|
+
const EXCLUDED_DIRS = ['.git', 'node_modules'];
|
|
11149
12263
|
function isSystemDirectoryPath(path) {
|
|
11150
12264
|
if (!path.startsWith('/'))
|
|
11151
12265
|
return false;
|
|
@@ -11171,6 +12285,7 @@ class FindingsLimitError extends Error {
|
|
|
11171
12285
|
}
|
|
11172
12286
|
}
|
|
11173
12287
|
exports.FindingsLimitError = FindingsLimitError;
|
|
12288
|
+
const NOT_SCANNABLE = 'scan path is not a regular file or directory';
|
|
11174
12289
|
/** At most this many files are open and read at once during a directory walk. */
|
|
11175
12290
|
exports.SCAN_CONCURRENCY = 16;
|
|
11176
12291
|
/** Bound on dispatched but not yet committed file results (memory bound). */
|
|
@@ -11247,13 +12362,54 @@ function tagMcpContext(path, text, findings) {
|
|
|
11247
12362
|
finding.context = 'mcp-config';
|
|
11248
12363
|
}
|
|
11249
12364
|
}
|
|
12365
|
+
/**
|
|
12366
|
+
* Opens a file for scanning: read-only, no-follow where the platform has it, and non-blocking,
|
|
12367
|
+
* so a FIFO swapped in after the walk's lstat cannot block open(2) waiting for a writer; the
|
|
12368
|
+
* caller then fstats the descriptor and reads only a regular file.
|
|
12369
|
+
*/
|
|
12370
|
+
exports.SCAN_OPEN_FLAGS = node_fs_1.constants.O_RDONLY | (node_fs_1.constants.O_NONBLOCK ?? 0) | (node_fs_1.constants.O_NOFOLLOW ?? 0);
|
|
12371
|
+
/**
|
|
12372
|
+
* The scan's file system with every operation raced against the interrupt (SIGINT, SIGTERM,
|
|
12373
|
+
* SIGHUP): an operation still pending when the scan is interrupted rejects with Cancelled at
|
|
12374
|
+
* once, so a slow or stuck file system call cannot hold the scan. A handle opened after the
|
|
12375
|
+
* interrupt is closed when its open completes; a close never waits for a stuck read.
|
|
12376
|
+
*/
|
|
12377
|
+
function cancellableFs(raw) {
|
|
12378
|
+
const wrap = (operation) => (...args) => (0, cancellation_js_1.cancellable)(() => operation(...args));
|
|
12379
|
+
const handle = (opened) => {
|
|
12380
|
+
const close = () => { const closing = opened.close(); closing.catch(() => { }); return (0, cancellation_js_1.cancellable)(() => closing); };
|
|
12381
|
+
// The scan uses only these three operations of a handle.
|
|
12382
|
+
return { stat: wrap(opened.stat.bind(opened)), read: wrap(opened.read.bind(opened)),
|
|
12383
|
+
close };
|
|
12384
|
+
};
|
|
12385
|
+
const open = async (...args) => {
|
|
12386
|
+
(0, cancellation_js_1.checkCancellation)();
|
|
12387
|
+
const pending = raw.open(...args);
|
|
12388
|
+
try {
|
|
12389
|
+
return handle(await (0, cancellation_js_1.cancellable)(() => pending));
|
|
12390
|
+
}
|
|
12391
|
+
catch (error) {
|
|
12392
|
+
if (error instanceof cancellation_js_1.Cancelled)
|
|
12393
|
+
pending.then(late => late.close()).catch(() => { });
|
|
12394
|
+
throw error;
|
|
12395
|
+
}
|
|
12396
|
+
};
|
|
12397
|
+
return { realpath: wrap(raw.realpath.bind(raw)),
|
|
12398
|
+
lstat: wrap(raw.lstat.bind(raw)),
|
|
12399
|
+
stat: wrap(raw.stat.bind(raw)),
|
|
12400
|
+
readdir: wrap(raw.readdir.bind(raw)),
|
|
12401
|
+
open };
|
|
12402
|
+
}
|
|
11250
12403
|
async function scan(paths, opts = {}) {
|
|
11251
|
-
const io = opts.fs ?? fs;
|
|
12404
|
+
const io = cancellableFs(opts.fs ?? fs);
|
|
11252
12405
|
const now = opts.clock?.() ?? new Date();
|
|
11253
12406
|
const findings = [];
|
|
11254
12407
|
const skipped = [];
|
|
11255
12408
|
const skippedPaths = new Set();
|
|
11256
12409
|
const scannedFiles = new Set();
|
|
12410
|
+
// Files with findings by identity, with the index of the path argument that scanned them.
|
|
12411
|
+
const owners = new Map();
|
|
12412
|
+
let currentArg = -1;
|
|
11257
12413
|
const secrets = new Set();
|
|
11258
12414
|
const ctx = { secrets, facts: new Map() };
|
|
11259
12415
|
let filesScanned = 0;
|
|
@@ -11270,7 +12426,7 @@ async function scan(paths, opts = {}) {
|
|
|
11270
12426
|
const target = await io.realpath(path);
|
|
11271
12427
|
if ((0, protected_path_js_1.isProtectedPath)(target) || isSystemDirectoryPath(target))
|
|
11272
12428
|
throw new index_js_3.InputError('input-read', 'cannot read ignore file');
|
|
11273
|
-
const handle = await io.open(target,
|
|
12429
|
+
const handle = await io.open(target, exports.SCAN_OPEN_FLAGS);
|
|
11274
12430
|
let content;
|
|
11275
12431
|
try {
|
|
11276
12432
|
const openedStat = await handle.stat({ bigint: true });
|
|
@@ -11284,7 +12440,9 @@ async function scan(paths, opts = {}) {
|
|
|
11284
12440
|
const bytes = await (0, bounded_file_js_1.readBounded)(handle, TEXT_LIMIT, openedStat.size);
|
|
11285
12441
|
if (bytes.length > TEXT_LIMIT)
|
|
11286
12442
|
throw new index_js_3.InputError('input-read', 'cannot read ignore file');
|
|
11287
|
-
|
|
12443
|
+
// Same encodings as scanned text: UTF-8 with or without BOM, UTF-16 LE or BE with a BOM
|
|
12444
|
+
// (what Windows PowerShell 5.1 writes with '>' and Out-File).
|
|
12445
|
+
content = decodeText(bytes);
|
|
11288
12446
|
}
|
|
11289
12447
|
finally {
|
|
11290
12448
|
await handle.close();
|
|
@@ -11294,7 +12452,7 @@ async function scan(paths, opts = {}) {
|
|
|
11294
12452
|
catch (error) {
|
|
11295
12453
|
if (optional && error.code === 'ENOENT')
|
|
11296
12454
|
return [];
|
|
11297
|
-
if (error instanceof index_js_3.InputError || error instanceof ignore_js_1.IgnoreFileError)
|
|
12455
|
+
if (error instanceof index_js_3.InputError || error instanceof ignore_js_1.IgnoreFileError || error instanceof cancellation_js_1.Cancelled)
|
|
11298
12456
|
throw error;
|
|
11299
12457
|
throw new index_js_3.InputError('input-read', 'cannot read ignore file');
|
|
11300
12458
|
}
|
|
@@ -11307,31 +12465,63 @@ async function scan(paths, opts = {}) {
|
|
|
11307
12465
|
skipped.push({ path: safePath, reason });
|
|
11308
12466
|
}
|
|
11309
12467
|
};
|
|
11310
|
-
|
|
12468
|
+
// True when `real` still names the directory the walk listed. lstat resolves every component
|
|
12469
|
+
// of the path, so a component replaced by a symlink (whose own identity differs) or by any
|
|
12470
|
+
// other directory since then shows up as a different identity. One lstat costs O(path
|
|
12471
|
+
// length), never a resolution per component.
|
|
12472
|
+
async function unchanged(parent) {
|
|
12473
|
+
return sameFileIdentity(await io.lstat(parent.real, { bigint: true }), parent.stat);
|
|
12474
|
+
}
|
|
12475
|
+
/**
|
|
12476
|
+
* Scans one file. An explicit argument is resolved with realpath. A walked file is not: its
|
|
12477
|
+
* resolved path is its parent's resolved path joined with its name, because the walk never
|
|
12478
|
+
* follows a symlink. Instead its own lstat, and its parent's identity, are checked again just
|
|
12479
|
+
* before and after the open; only an entry that changed since the walk's lstat is resolved
|
|
12480
|
+
* (realpath), to tell a swapped-in link to a protected file (never-open) from other changes.
|
|
12481
|
+
*/
|
|
12482
|
+
async function scanFileTask(path, sourceStat, explicit, parent) {
|
|
11311
12483
|
let identity;
|
|
11312
12484
|
try {
|
|
11313
|
-
|
|
11314
|
-
if (
|
|
11315
|
-
|
|
12485
|
+
let target;
|
|
12486
|
+
if (parent === undefined) {
|
|
12487
|
+
target = await io.realpath(path);
|
|
12488
|
+
if ((0, protected_path_js_1.isProtectedPath)(target))
|
|
12489
|
+
return { type: 'skip', path, reason: 'never-open' };
|
|
12490
|
+
}
|
|
12491
|
+
else {
|
|
12492
|
+
target = (0, node_path_1.join)(parent.real, (0, node_path_1.basename)(path));
|
|
12493
|
+
const current = await io.lstat(target, { bigint: true });
|
|
12494
|
+
if (current.isSymbolicLink() || !current.isFile()) {
|
|
12495
|
+
if ((0, protected_path_js_1.isProtectedPath)(await io.realpath(target)))
|
|
12496
|
+
return { type: 'skip', path, reason: 'never-open' };
|
|
12497
|
+
return { type: 'skip', path, reason: current.isSymbolicLink() ? 'symlink' : 'not-regular' };
|
|
12498
|
+
}
|
|
12499
|
+
if (!await unchanged(parent))
|
|
12500
|
+
throw new index_js_3.InputError('input-read');
|
|
12501
|
+
}
|
|
11316
12502
|
const macaroonFile = /\.macaroon$/i.test((0, node_path_1.basename)(path));
|
|
11317
12503
|
if (!macaroonFile && BigInt(sourceStat.size) > BigInt(TEXT_LIMIT))
|
|
11318
12504
|
return { type: 'skip', path, reason: 'size' };
|
|
11319
12505
|
let handle;
|
|
11320
12506
|
let bytes;
|
|
11321
12507
|
try {
|
|
11322
|
-
handle = await io.open(target,
|
|
12508
|
+
handle = await io.open(target, exports.SCAN_OPEN_FLAGS);
|
|
11323
12509
|
const openedStat = await handle.stat({ bigint: true });
|
|
11324
|
-
if (!openedStat.isFile())
|
|
12510
|
+
if (!openedStat.isFile()) {
|
|
12511
|
+
if (explicit)
|
|
12512
|
+
return { type: 'error', error: new index_js_3.InputError('input-read', NOT_SCANNABLE) };
|
|
11325
12513
|
return { type: 'skip', path, reason: 'not-regular' };
|
|
11326
|
-
|
|
11327
|
-
|
|
12514
|
+
}
|
|
12515
|
+
// Check the path again after open, including platforms without O_NOFOLLOW: it must
|
|
12516
|
+
// still be the regular file that was opened, under the same resolved path.
|
|
11328
12517
|
const openedPathStat = await io.lstat(target, { bigint: true });
|
|
11329
|
-
|
|
12518
|
+
const samePath = parent === undefined ? await io.realpath(target).then(openedPath => !(0, protected_path_js_1.isProtectedPath)(openedPath) && openedPath === target) : await unchanged(parent);
|
|
12519
|
+
if (!samePath || !openedPathStat.isFile() ||
|
|
11330
12520
|
openedPathStat.isSymbolicLink() || !sameFileIdentity(openedPathStat, openedStat))
|
|
11331
12521
|
throw new index_js_3.InputError('input-read');
|
|
11332
12522
|
identity = fileIdentity(openedStat, target);
|
|
11333
12523
|
if (scannedFiles.has(identity))
|
|
11334
|
-
return { type: 'dup' };
|
|
12524
|
+
return { type: 'dup', path, identity };
|
|
11335
12525
|
bytes = await (0, bounded_file_js_1.readBounded)(handle, macaroonFile ? MACAROON_LIMIT : TEXT_LIMIT, openedStat.size);
|
|
11336
12526
|
}
|
|
11337
12527
|
finally {
|
|
@@ -11346,9 +12536,11 @@ async function scan(paths, opts = {}) {
|
|
|
11346
12536
|
for (const finding of outcome.findings)
|
|
11347
12537
|
finding.path = safePath;
|
|
11348
12538
|
}
|
|
11349
|
-
return { type: 'file', identity, findings: outcome.findings, dropped: outcome.dropped };
|
|
12539
|
+
return { type: 'file', path, identity, findings: outcome.findings, dropped: outcome.dropped };
|
|
11350
12540
|
}
|
|
11351
|
-
catch {
|
|
12541
|
+
catch (error) {
|
|
12542
|
+
if (error instanceof cancellation_js_1.Cancelled)
|
|
12543
|
+
throw error;
|
|
11352
12544
|
if (explicit)
|
|
11353
12545
|
return { type: 'error', error: new index_js_3.InputError('input-read', 'cannot read scan path') };
|
|
11354
12546
|
return { type: 'skip', path, reason: 'unreadable', identity };
|
|
@@ -11359,15 +12551,34 @@ async function scan(paths, opts = {}) {
|
|
|
11359
12551
|
throw new FindingsLimitError();
|
|
11360
12552
|
findings.push(finding);
|
|
11361
12553
|
}
|
|
12554
|
+
// Another name of a file already scanned under the same path argument (a hard link) is
|
|
12555
|
+
// added to the alsoAt of that file's findings, so every path holding the credential is
|
|
12556
|
+
// reported. Names reached through a different argument stay silent (SPEC: several arguments).
|
|
12557
|
+
function alias(identity, path) {
|
|
12558
|
+
const owner = owners.get(identity);
|
|
12559
|
+
if (!owner || owner.arg !== currentArg)
|
|
12560
|
+
return;
|
|
12561
|
+
const safePath = (0, redact_js_1.redactPath)(reportedPath(path));
|
|
12562
|
+
for (const finding of owner.findings) {
|
|
12563
|
+
const also = finding.alsoAt ??= { count: 0, paths: [] };
|
|
12564
|
+
also.count++;
|
|
12565
|
+
if (also.paths.length < exports.ALSO_AT_EXAMPLES)
|
|
12566
|
+
also.paths.push(safePath);
|
|
12567
|
+
}
|
|
12568
|
+
}
|
|
11362
12569
|
// Applies one event. A file whose identity was already committed is a
|
|
11363
12570
|
// duplicate, exactly as the sequential walk's check before reading.
|
|
11364
12571
|
function commit(event) {
|
|
11365
12572
|
if (event.type === 'error')
|
|
11366
12573
|
throw event.error;
|
|
11367
|
-
if (event.type === 'dup')
|
|
12574
|
+
if (event.type === 'dup') {
|
|
12575
|
+
alias(event.identity, event.path);
|
|
11368
12576
|
return;
|
|
11369
|
-
|
|
12577
|
+
}
|
|
12578
|
+
if (event.identity !== undefined && scannedFiles.has(event.identity)) {
|
|
12579
|
+
alias(event.identity, event.path);
|
|
11370
12580
|
return;
|
|
12581
|
+
}
|
|
11371
12582
|
if (event.type === 'skip') {
|
|
11372
12583
|
skip(event.path, event.reason);
|
|
11373
12584
|
return;
|
|
@@ -11377,6 +12588,8 @@ async function scan(paths, opts = {}) {
|
|
|
11377
12588
|
dropped += event.dropped;
|
|
11378
12589
|
filesScanned++;
|
|
11379
12590
|
scannedFiles.add(event.identity);
|
|
12591
|
+
if (event.findings.length)
|
|
12592
|
+
owners.set(event.identity, { arg: currentArg, findings: event.findings });
|
|
11380
12593
|
}
|
|
11381
12594
|
// Ordered, bounded dispatch: at most SCAN_CONCURRENCY file tasks run, and at
|
|
11382
12595
|
// most SCAN_QUEUE results wait for earlier ones before being committed.
|
|
@@ -11405,12 +12618,12 @@ async function scan(paths, opts = {}) {
|
|
|
11405
12618
|
ready.set(dispatched++, event);
|
|
11406
12619
|
drainReady();
|
|
11407
12620
|
}
|
|
11408
|
-
async function dispatch(path, stat) {
|
|
12621
|
+
async function dispatch(path, stat, parent) {
|
|
11409
12622
|
(0, cancellation_js_1.checkCancellation)();
|
|
11410
12623
|
await waitFor(() => running < exports.SCAN_CONCURRENCY && dispatched - committed < SCAN_QUEUE);
|
|
11411
12624
|
const sequence = dispatched++;
|
|
11412
12625
|
running++;
|
|
11413
|
-
void scanFileTask(path, stat, false)
|
|
12626
|
+
void scanFileTask(path, stat, false, parent)
|
|
11414
12627
|
.catch((error) => ({ type: 'error', error }))
|
|
11415
12628
|
.then(event => { running--; ready.set(sequence, event); signal(); });
|
|
11416
12629
|
}
|
|
@@ -11422,7 +12635,13 @@ async function scan(paths, opts = {}) {
|
|
|
11422
12635
|
await settle();
|
|
11423
12636
|
commit(await scanFileTask(path, sourceStat, explicit));
|
|
11424
12637
|
}
|
|
11425
|
-
|
|
12638
|
+
// An interrupt ends the scan; it is never turned into an unreadable skip or exit 3.
|
|
12639
|
+
const rethrowCancelled = (error) => { if (error instanceof cancellation_js_1.Cancelled)
|
|
12640
|
+
throw error; };
|
|
12641
|
+
// `parent` is the walked directory that listed `path` (absent for a path argument). Resolved
|
|
12642
|
+
// paths are carried down the walk instead of calling realpath per entry: realpath resolves
|
|
12643
|
+
// every component, so per entry it made the walk's cost grow with the cube of its depth.
|
|
12644
|
+
async function walk(path, isRoot, rootStat, known, parent, rootReal) {
|
|
11426
12645
|
(0, cancellation_js_1.checkCancellation)();
|
|
11427
12646
|
const note = (target, reason) => emit({ type: 'skip', path: target, reason });
|
|
11428
12647
|
if ((0, protected_path_js_1.isProtectedPath)(path)) {
|
|
@@ -11441,7 +12660,8 @@ async function scan(paths, opts = {}) {
|
|
|
11441
12660
|
try {
|
|
11442
12661
|
entry = await io.lstat(path, { bigint: true });
|
|
11443
12662
|
}
|
|
11444
|
-
catch {
|
|
12663
|
+
catch (error) {
|
|
12664
|
+
rethrowCancelled(error);
|
|
11445
12665
|
if (isRoot)
|
|
11446
12666
|
throw new index_js_3.InputError('input-read', 'cannot read scan path');
|
|
11447
12667
|
await note(path, 'unreadable');
|
|
@@ -11455,14 +12675,23 @@ async function scan(paths, opts = {}) {
|
|
|
11455
12675
|
const stat = rootStat ?? entry;
|
|
11456
12676
|
if (stat.isDirectory()) {
|
|
11457
12677
|
let target;
|
|
11458
|
-
|
|
11459
|
-
target =
|
|
12678
|
+
if (rootReal !== undefined)
|
|
12679
|
+
target = rootReal;
|
|
12680
|
+
else if (parent === undefined) {
|
|
12681
|
+
try {
|
|
12682
|
+
target = await io.realpath(path);
|
|
12683
|
+
}
|
|
12684
|
+
catch (error) {
|
|
12685
|
+
rethrowCancelled(error);
|
|
12686
|
+
if (isRoot)
|
|
12687
|
+
throw new index_js_3.InputError('input-read', 'cannot read scan path');
|
|
12688
|
+
await note(path, 'unreadable');
|
|
12689
|
+
return;
|
|
12690
|
+
}
|
|
11460
12691
|
}
|
|
11461
|
-
|
|
11462
|
-
|
|
11463
|
-
|
|
11464
|
-
await note(path, 'unreadable');
|
|
11465
|
-
return;
|
|
12692
|
+
else {
|
|
12693
|
+
// Not a symlink (refused above), so it resolves to its parent's resolved path plus its name.
|
|
12694
|
+
target = (0, node_path_1.join)(parent.real, (0, node_path_1.basename)(path));
|
|
11466
12695
|
}
|
|
11467
12696
|
if ((0, protected_path_js_1.isProtectedPath)(target)) {
|
|
11468
12697
|
await note(path, 'never-open');
|
|
@@ -11472,8 +12701,7 @@ async function scan(paths, opts = {}) {
|
|
|
11472
12701
|
await note(path, 'excluded-dir');
|
|
11473
12702
|
return;
|
|
11474
12703
|
}
|
|
11475
|
-
|
|
11476
|
-
if (!opts.includeDirs && (name === '.git' || name === 'node_modules')) {
|
|
12704
|
+
if (!opts.includeDirs && namedAs((0, node_path_1.basename)(path), EXCLUDED_DIRS)) {
|
|
11477
12705
|
await note(path, 'excluded-dir');
|
|
11478
12706
|
return;
|
|
11479
12707
|
}
|
|
@@ -11481,43 +12709,62 @@ async function scan(paths, opts = {}) {
|
|
|
11481
12709
|
try {
|
|
11482
12710
|
names = await io.readdir(path);
|
|
11483
12711
|
}
|
|
11484
|
-
catch {
|
|
12712
|
+
catch (error) {
|
|
12713
|
+
rethrowCancelled(error);
|
|
11485
12714
|
if (isRoot)
|
|
11486
12715
|
throw new index_js_3.InputError('input-read', 'cannot read scan path');
|
|
11487
12716
|
await note(path, 'unreadable');
|
|
11488
12717
|
return;
|
|
11489
12718
|
}
|
|
11490
12719
|
const rootListing = isRoot && reportedPath(path) === '/';
|
|
12720
|
+
const self = { real: target, stat };
|
|
11491
12721
|
// Stat children in bounded batches, then visit them in listing order.
|
|
11492
12722
|
// Protected and excluded names are never stat'ed, as before.
|
|
11493
12723
|
for (let offset = 0; offset < names.length; offset += exports.SCAN_CONCURRENCY) {
|
|
11494
12724
|
const batch = names.slice(offset, offset + exports.SCAN_CONCURRENCY).map(child => (0, node_path_1.join)(path, child));
|
|
11495
12725
|
const entries = await Promise.all(batch.map(async (child) => {
|
|
11496
|
-
if (rootListing && ['proc', 'sys', 'dev']
|
|
12726
|
+
if (rootListing && namedAs((0, node_path_1.basename)(child), ['proc', 'sys', 'dev']))
|
|
11497
12727
|
return { excluded: true };
|
|
11498
12728
|
if ((0, protected_path_js_1.isProtectedPath)(child))
|
|
11499
12729
|
return { protected: true };
|
|
11500
12730
|
try {
|
|
11501
12731
|
return { stat: await io.lstat(child, { bigint: true }) };
|
|
11502
12732
|
}
|
|
11503
|
-
catch {
|
|
12733
|
+
catch (error) {
|
|
12734
|
+
rethrowCancelled(error);
|
|
11504
12735
|
return { error: true };
|
|
11505
12736
|
}
|
|
11506
12737
|
}));
|
|
12738
|
+
// The children's stats count only if this directory is still the one listed: a path
|
|
12739
|
+
// component swapped for a symlink would otherwise lead the walk outside the tree.
|
|
12740
|
+
let same;
|
|
12741
|
+
try {
|
|
12742
|
+
same = await unchanged(self);
|
|
12743
|
+
}
|
|
12744
|
+
catch (error) {
|
|
12745
|
+
rethrowCancelled(error);
|
|
12746
|
+
same = false;
|
|
12747
|
+
}
|
|
12748
|
+
if (!same) {
|
|
12749
|
+
if (isRoot)
|
|
12750
|
+
throw new index_js_3.InputError('input-read', 'cannot read scan path');
|
|
12751
|
+
await note(path, 'unreadable');
|
|
12752
|
+
return;
|
|
12753
|
+
}
|
|
11507
12754
|
for (let i = 0; i < batch.length; i++) {
|
|
11508
12755
|
const known = entries[i];
|
|
11509
12756
|
if ('excluded' in known)
|
|
11510
12757
|
await note(batch[i], 'excluded-dir');
|
|
11511
12758
|
else
|
|
11512
|
-
await walk(batch[i], false, undefined, known);
|
|
12759
|
+
await walk(batch[i], false, undefined, known, self);
|
|
11513
12760
|
}
|
|
11514
12761
|
}
|
|
11515
12762
|
}
|
|
11516
12763
|
else if (stat.isFile()) {
|
|
11517
|
-
if (isRoot)
|
|
12764
|
+
if (isRoot || parent === undefined)
|
|
11518
12765
|
await scanFile(path, stat, true);
|
|
11519
12766
|
else
|
|
11520
|
-
await dispatch(path, stat);
|
|
12767
|
+
await dispatch(path, stat, parent);
|
|
11521
12768
|
}
|
|
11522
12769
|
else {
|
|
11523
12770
|
await note(path, 'not-regular');
|
|
@@ -11546,7 +12793,9 @@ async function scan(paths, opts = {}) {
|
|
|
11546
12793
|
shallowHistory = true;
|
|
11547
12794
|
}
|
|
11548
12795
|
// A credential still present in the work tree is already reported by the walk.
|
|
11549
|
-
|
|
12796
|
+
// That includes a work-tree file's other hard-link names (its alsoAt).
|
|
12797
|
+
const workTree = new Set(findings.flatMap(finding => [finding.path, ...(finding.commit === undefined ? finding.alsoAt?.paths ?? [] : [])]
|
|
12798
|
+
.map(path => `${path}\0${finding.fingerprint}`)));
|
|
11550
12799
|
const table = await (0, git_history_js_1.historyEntries)(git, directory, repository, pathspec);
|
|
11551
12800
|
// Per distinct path: CLASSIFIED, MACAROON (*.macaroon), EXCLUDED (protected or in an
|
|
11552
12801
|
// excluded directory), and whether a skip was already reported for it (skips are
|
|
@@ -11565,8 +12814,8 @@ async function scan(paths, opts = {}) {
|
|
|
11565
12814
|
pathState[p] |= CLASSIFIED;
|
|
11566
12815
|
const relativePath = table.paths[p];
|
|
11567
12816
|
const parents = relativePath.split('/').slice(0, -1);
|
|
11568
|
-
// Same comparison as the file walk
|
|
11569
|
-
const excluded = parents.findIndex(segment =>
|
|
12817
|
+
// Same comparison as the file walk, so history and work tree agree.
|
|
12818
|
+
const excluded = parents.findIndex(segment => namedAs(segment, EXCLUDED_DIRS));
|
|
11570
12819
|
if ((0, protected_path_js_1.isProtectedPath)(joined(p))) {
|
|
11571
12820
|
skip(joined(p), 'never-open');
|
|
11572
12821
|
pathState[p] |= EXCLUDED;
|
|
@@ -11726,13 +12975,15 @@ async function scan(paths, opts = {}) {
|
|
|
11726
12975
|
}
|
|
11727
12976
|
}
|
|
11728
12977
|
for (const arg of paths) {
|
|
12978
|
+
currentArg++;
|
|
11729
12979
|
if ((0, protected_path_js_1.isProtectedPath)(arg)) {
|
|
11730
12980
|
skip(arg, 'never-open');
|
|
11731
12981
|
continue;
|
|
11732
12982
|
}
|
|
11733
12983
|
let stat;
|
|
12984
|
+
let target;
|
|
11734
12985
|
try {
|
|
11735
|
-
|
|
12986
|
+
target = await io.realpath(arg);
|
|
11736
12987
|
if ((0, protected_path_js_1.isProtectedPath)(target)) {
|
|
11737
12988
|
skip(arg, 'never-open');
|
|
11738
12989
|
continue;
|
|
@@ -11743,12 +12994,19 @@ async function scan(paths, opts = {}) {
|
|
|
11743
12994
|
}
|
|
11744
12995
|
stat = await io.stat(arg, { bigint: true });
|
|
11745
12996
|
}
|
|
11746
|
-
catch {
|
|
12997
|
+
catch (error) {
|
|
12998
|
+
if (error instanceof cancellation_js_1.Cancelled)
|
|
12999
|
+
throw error;
|
|
11747
13000
|
throw new index_js_3.InputError('input-read', 'cannot read scan path');
|
|
11748
13001
|
}
|
|
13002
|
+
// A named FIFO, device or socket cannot be scanned. Skipping it would let a CI gate pass
|
|
13003
|
+
// having scanned nothing, so it fails like an unreadable path.
|
|
13004
|
+
if (!stat.isFile() && !stat.isDirectory())
|
|
13005
|
+
throw new index_js_3.InputError('input-read', NOT_SCANNABLE);
|
|
11749
13006
|
const entries = explicitIgnore ?? (stat.isDirectory() ? await readIgnoreFile((0, node_path_1.join)(arg, '.macwardenignore'), true) : []);
|
|
11750
13007
|
const firstFinding = findings.length;
|
|
11751
|
-
|
|
13008
|
+
// The argument was resolved above; the walk starts from that resolved path.
|
|
13009
|
+
await walk(arg, true, stat, undefined, undefined, target);
|
|
11752
13010
|
await settle();
|
|
11753
13011
|
if (opts.gitHistory)
|
|
11754
13012
|
await scanHistory(arg, stat);
|
|
@@ -11770,7 +13028,7 @@ async function scan(paths, opts = {}) {
|
|
|
11770
13028
|
let drop = false;
|
|
11771
13029
|
if (candidates) {
|
|
11772
13030
|
const ignoredAt = (path) => {
|
|
11773
|
-
const matchPath = explicitIgnore === null ? (0, node_path_1.relative)(arg, path).
|
|
13031
|
+
const matchPath = explicitIgnore === null ? (0, node_path_1.relative)(arg, path).split(node_path_1.sep).join('/') : path;
|
|
11774
13032
|
const key = `${finding.fingerprint}\0${matchPath}`;
|
|
11775
13033
|
const known = decided.get(key);
|
|
11776
13034
|
const result = known ?? candidates.some(entry => entry.matches(matchPath));
|
|
@@ -12045,7 +13303,12 @@ const PREFIX = /(?:nostr\+walletconnect|nostrwalletconnect):(?:\/\/)?[0-9a-f]{64
|
|
|
12045
13303
|
const BOUNDARY = /[\s"'<>]/;
|
|
12046
13304
|
const HEX = /[0-9a-f]/i;
|
|
12047
13305
|
const SECRET = 'secret=';
|
|
12048
|
-
|
|
13306
|
+
/**
|
|
13307
|
+
* `open`, when given and empty, receives the index of the first prefix (outside an earlier
|
|
13308
|
+
* match) whose middle runs to the end of `text` without a boundary or a valid secret: in a
|
|
13309
|
+
* window cut from a longer line, such a URI may continue past the window end.
|
|
13310
|
+
*/
|
|
13311
|
+
function nwcMatches(text, open) {
|
|
12049
13312
|
const length = text.length;
|
|
12050
13313
|
// nextValid[i]: smallest p >= i where "secret=" + 64 hex + (non-hex | end) starts.
|
|
12051
13314
|
// nextBoundary[i]: smallest p >= i holding a boundary character. Both scanned right to left.
|
|
@@ -12074,6 +13337,9 @@ function nwcMatches(text) {
|
|
|
12074
13337
|
const middle = head.index + head[0].length;
|
|
12075
13338
|
const secretAt = nextValid[middle];
|
|
12076
13339
|
const boundaryAt = nextBoundary[middle];
|
|
13340
|
+
// Every later prefix lies in the same open run, so only the first is reported.
|
|
13341
|
+
if (secretAt === -1 && boundaryAt === -1 && open?.length === 0)
|
|
13342
|
+
open.push(head.index);
|
|
12077
13343
|
if (secretAt === -1 || (boundaryAt !== -1 && boundaryAt < secretAt))
|
|
12078
13344
|
continue;
|
|
12079
13345
|
const afterHex = secretAt + SECRET.length + 64;
|
|
@@ -13081,6 +14347,8 @@ const nodeFs = __importStar(require("node:fs/promises"));
|
|
|
13081
14347
|
const node_crypto_1 = require("node:crypto");
|
|
13082
14348
|
const node_path_1 = require("node:path");
|
|
13083
14349
|
const bounded_file_js_1 = require("../bounded-file.js");
|
|
14350
|
+
const instant_js_1 = require("../instant.js");
|
|
14351
|
+
const root_key_id_js_1 = require("../root-key-id.js");
|
|
13084
14352
|
// Callers choose how many distinct (credential, method) pairs exist, so the store is
|
|
13085
14353
|
// bounded. At capacity the least recently seen record is evicted (O(1): the Map keeps
|
|
13086
14354
|
// records in recency order), and the eviction is remembered so `recommend` can tell
|
|
@@ -13113,9 +14381,12 @@ class UsageFileError extends Error {
|
|
|
13113
14381
|
}
|
|
13114
14382
|
exports.UsageFileError = UsageFileError;
|
|
13115
14383
|
const fields = ['nodePubkey', 'credential', 'rootKeyId', 'method', 'calls', 'errors', 'firstSeen', 'lastSeen'];
|
|
13116
|
-
|
|
14384
|
+
// Timestamps must be real instants (isInstant) and each pair in order, compared as instants:
|
|
14385
|
+
// evidence with an impossible or reversed time is refused, never used for time arithmetic.
|
|
14386
|
+
const ordered = (first, last) => (0, instant_js_1.isInstant)(first) && (0, instant_js_1.isInstant)(last) && Date.parse(first) <= Date.parse(last);
|
|
14387
|
+
/** The later of two instants (a clock stepping back never moves a "last" time before its "first"). */
|
|
14388
|
+
const later = (current, now) => Date.parse(now) >= Date.parse(current) ? now : current;
|
|
13117
14389
|
const NODE = /^0[23][0-9a-f]{64}$/;
|
|
13118
|
-
const ROOT = /^(0|[1-9][0-9]{0,19})$/;
|
|
13119
14390
|
const exactKeys = (record, names) => {
|
|
13120
14391
|
const keys = Object.keys(record);
|
|
13121
14392
|
return keys.length === names.length && keys.every(key => names.includes(key));
|
|
@@ -13125,15 +14396,13 @@ const object = (value) => !!value && typeof value === 'object' && !Array.isArray
|
|
|
13125
14396
|
function validCanaryUse(value) {
|
|
13126
14397
|
return object(value) && exactKeys(value, ['nodePubkey', 'rootKeyId', 'uses', 'firstUse', 'lastUse'])
|
|
13127
14398
|
&& typeof value.nodePubkey === 'string' && NODE.test(value.nodePubkey)
|
|
13128
|
-
&&
|
|
13129
|
-
&& count(value.uses) &&
|
|
13130
|
-
&& typeof value.lastUse === 'string' && time.test(value.lastUse);
|
|
14399
|
+
&& (0, root_key_id_js_1.isRootKeyId)(value.rootKeyId)
|
|
14400
|
+
&& count(value.uses) && ordered(value.firstUse, value.lastUse);
|
|
13131
14401
|
}
|
|
13132
14402
|
function validCoverage(value) {
|
|
13133
14403
|
return object(value) && exactKeys(value, ['nodePubkey', 'firstStart', 'lastEnd', 'coveredSeconds', 'sessions'])
|
|
13134
14404
|
&& typeof value.nodePubkey === 'string' && NODE.test(value.nodePubkey)
|
|
13135
|
-
&&
|
|
13136
|
-
&& typeof value.lastEnd === 'string' && time.test(value.lastEnd)
|
|
14405
|
+
&& ordered(value.firstStart, value.lastEnd)
|
|
13137
14406
|
&& count(value.coveredSeconds) && count(value.sessions);
|
|
13138
14407
|
}
|
|
13139
14408
|
function validSpans(value) {
|
|
@@ -13141,7 +14410,7 @@ function validSpans(value) {
|
|
|
13141
14410
|
&& typeof value.nodePubkey === 'string' && NODE.test(value.nodePubkey)
|
|
13142
14411
|
&& Array.isArray(value.spans) && value.spans.length <= exports.SPAN_LIMIT
|
|
13143
14412
|
&& value.spans.every(span => Array.isArray(span) && span.length === 2
|
|
13144
|
-
&&
|
|
14413
|
+
&& ordered(span[0], span[1]));
|
|
13145
14414
|
}
|
|
13146
14415
|
/** The union of spans: overlapping or touching ones merged, so no time is counted twice. */
|
|
13147
14416
|
function union(spans) {
|
|
@@ -13163,12 +14432,11 @@ function validRecord(value) {
|
|
|
13163
14432
|
return keys.length === fields.length && keys.every(key => fields.includes(key))
|
|
13164
14433
|
&& typeof record.nodePubkey === 'string' && /^0[23][0-9a-f]{64}$/.test(record.nodePubkey)
|
|
13165
14434
|
&& typeof record.credential === 'string' && /^id:[0-9a-f]{16}$/.test(record.credential)
|
|
13166
|
-
&&
|
|
14435
|
+
&& (0, root_key_id_js_1.isRootKeyId)(record.rootKeyId)
|
|
13167
14436
|
&& typeof record.method === 'string' && /^\/[A-Za-z0-9_.]+\/[A-Za-z0-9_]+$/.test(record.method)
|
|
13168
14437
|
&& Number.isSafeInteger(record.calls) && record.calls >= 0
|
|
13169
14438
|
&& Number.isSafeInteger(record.errors) && record.errors >= 0
|
|
13170
|
-
&&
|
|
13171
|
-
&& typeof record.lastSeen === 'string' && time.test(record.lastSeen);
|
|
14439
|
+
&& ordered(record.firstSeen, record.lastSeen);
|
|
13172
14440
|
}
|
|
13173
14441
|
class UsageStore {
|
|
13174
14442
|
limit;
|
|
@@ -13201,7 +14469,7 @@ class UsageStore {
|
|
|
13201
14469
|
touch() { this.changed = true; this.version += 1; }
|
|
13202
14470
|
/** Counts denied uses of a registered canary. */
|
|
13203
14471
|
recordCanary(nodePubkey, rootKeyId, uses, at) {
|
|
13204
|
-
if (!NODE.test(nodePubkey) || !
|
|
14472
|
+
if (!NODE.test(nodePubkey) || !(0, root_key_id_js_1.isRootKeyId)(rootKeyId) || !Number.isSafeInteger(uses) || uses < 1)
|
|
13205
14473
|
return;
|
|
13206
14474
|
const key = `${nodePubkey}\0${rootKeyId}`;
|
|
13207
14475
|
const now = at.toISOString();
|
|
@@ -13210,7 +14478,7 @@ class UsageStore {
|
|
|
13210
14478
|
if (!existing && this.canaryUses.size >= exports.CANARY_LIMIT)
|
|
13211
14479
|
this.canaryUses.delete(this.canaryUses.keys().next().value);
|
|
13212
14480
|
this.canaryUses.set(key, existing
|
|
13213
|
-
? { ...existing, uses: Math.min(existing.uses + uses, Number.MAX_SAFE_INTEGER), lastUse: now }
|
|
14481
|
+
? { ...existing, uses: Math.min(existing.uses + uses, Number.MAX_SAFE_INTEGER), lastUse: later(existing.lastUse, now) }
|
|
13214
14482
|
: { nodePubkey, rootKeyId, uses, firstUse: now, lastUse: now });
|
|
13215
14483
|
this.touch();
|
|
13216
14484
|
}
|
|
@@ -13230,7 +14498,7 @@ class UsageStore {
|
|
|
13230
14498
|
this.coverages.delete(oldest);
|
|
13231
14499
|
this.spans.delete(oldest);
|
|
13232
14500
|
}
|
|
13233
|
-
this.coverages.set(nodePubkey, existing ? { ...existing, sessions: existing.sessions + 1, lastEnd: now }
|
|
14501
|
+
this.coverages.set(nodePubkey, existing ? { ...existing, sessions: existing.sessions + 1, lastEnd: later(existing.lastEnd, now) }
|
|
13234
14502
|
: { nodePubkey, firstStart: now, lastEnd: now, coveredSeconds: 0, sessions: 1 });
|
|
13235
14503
|
this.covering.set(nodePubkey, at.getTime());
|
|
13236
14504
|
// A new span, starting no earlier than the last one ended (a clock stepping back never
|
|
@@ -13264,7 +14532,7 @@ class UsageStore {
|
|
|
13264
14532
|
const span = this.spans.get(nodePubkey)?.at(-1);
|
|
13265
14533
|
if (span && until > span[1])
|
|
13266
14534
|
span[1] = until;
|
|
13267
|
-
record.lastEnd = at.toISOString();
|
|
14535
|
+
record.lastEnd = later(record.lastEnd, at.toISOString());
|
|
13268
14536
|
this.touch();
|
|
13269
14537
|
}
|
|
13270
14538
|
/** The registration ended (stream lost, run stopped). */
|
|
@@ -13301,7 +14569,7 @@ class UsageStore {
|
|
|
13301
14569
|
*/
|
|
13302
14570
|
record(nodePubkey, credential, rootKeyId, method, at, error = false) {
|
|
13303
14571
|
if (!/^0[23][0-9a-f]{64}$/.test(nodePubkey) || !/^id:[0-9a-f]{16}$/.test(credential)
|
|
13304
|
-
||
|
|
14572
|
+
|| !(0, root_key_id_js_1.isRootKeyId)(rootKeyId)) {
|
|
13305
14573
|
this.refused += 1;
|
|
13306
14574
|
return false;
|
|
13307
14575
|
}
|
|
@@ -13314,7 +14582,7 @@ class UsageStore {
|
|
|
13314
14582
|
existing.errors += 1;
|
|
13315
14583
|
else
|
|
13316
14584
|
existing.calls += 1;
|
|
13317
|
-
existing.lastSeen = now;
|
|
14585
|
+
existing.lastSeen = later(existing.lastSeen, now);
|
|
13318
14586
|
// Move to the end: the first record in the Map is always the least recently seen.
|
|
13319
14587
|
this.records.delete(key);
|
|
13320
14588
|
this.records.set(key, existing);
|
|
@@ -13388,7 +14656,7 @@ class UsageStore {
|
|
|
13388
14656
|
// are required; known later fields are validated when present; unknown fields are
|
|
13389
14657
|
// ignored, so a newer file stays readable by this version.
|
|
13390
14658
|
const count = (field) => field === undefined || (Number.isSafeInteger(field) && field >= 0);
|
|
13391
|
-
const instant = (field) => field === undefined || field === null || (
|
|
14659
|
+
const instant = (field) => field === undefined || field === null || (0, instant_js_1.isInstant)(field);
|
|
13392
14660
|
if (value.schema !== 1 || !Number.isSafeInteger(value.dropped) || value.dropped < 0
|
|
13393
14661
|
|| !count(value.evictions) || !instant(value.lastEviction)
|
|
13394
14662
|
|| !count(value.droppedAttributions) || !instant(value.lastAttributionDrop)
|
|
@@ -14018,7 +15286,7 @@ function load(id) { if (id === 'grade/lnd-permissions.json') return snapshot; if
|
|
|
14018
15286
|
} return load(parts.join('/')); }); return module.exports; }
|
|
14019
15287
|
export const api0_readBounded = load("bounded-file.js").readBounded;
|
|
14020
15288
|
export const api0_readBoundedFile = load("bounded-file.js").readBoundedFile;
|
|
14021
|
-
export const
|
|
15289
|
+
export const api0_readPlainFile = load("bounded-file.js").readPlainFile;
|
|
14022
15290
|
export const api0_BoundedFileError = load("bounded-file.js").BoundedFileError;
|
|
14023
15291
|
export const api0_DEFAULT_FILE_LIMIT = load("bounded-file.js").DEFAULT_FILE_LIMIT;
|
|
14024
15292
|
export const api0_LOCK_FILE_LIMIT = load("bounded-file.js").LOCK_FILE_LIMIT;
|
|
@@ -14040,235 +15308,254 @@ export const api5_attenuate = load("commands/attenuate.js").attenuate;
|
|
|
14040
15308
|
export const api6_allocateRootKeyId = load("commands/bake/allocate.js").allocateRootKeyId;
|
|
14041
15309
|
export const api7_run = load("commands/bake/index.js").run;
|
|
14042
15310
|
export const api7_bake = load("commands/bake/index.js").bake;
|
|
14043
|
-
export const
|
|
14044
|
-
export const
|
|
14045
|
-
export const
|
|
14046
|
-
export const
|
|
14047
|
-
export const
|
|
14048
|
-
export const api11_run = load("commands/
|
|
14049
|
-
export const
|
|
14050
|
-
export const
|
|
14051
|
-
export const
|
|
14052
|
-
export const
|
|
14053
|
-
export const
|
|
14054
|
-
export const
|
|
14055
|
-
export const
|
|
14056
|
-
export const
|
|
14057
|
-
export const
|
|
14058
|
-
export const
|
|
14059
|
-
export const
|
|
14060
|
-
export const
|
|
14061
|
-
export const
|
|
14062
|
-
export const
|
|
14063
|
-
export const
|
|
14064
|
-
export const
|
|
14065
|
-
export const api15_run = load("commands/
|
|
14066
|
-
export const
|
|
14067
|
-
export const api16_run = load("commands/
|
|
14068
|
-
export const
|
|
14069
|
-
export const
|
|
14070
|
-
export const
|
|
14071
|
-
export const
|
|
14072
|
-
export const
|
|
14073
|
-
export const
|
|
14074
|
-
export const
|
|
14075
|
-
export const
|
|
14076
|
-
export const
|
|
14077
|
-
export const
|
|
14078
|
-
export const
|
|
14079
|
-
export const
|
|
14080
|
-
export const
|
|
14081
|
-
export const
|
|
14082
|
-
export const
|
|
14083
|
-
export const
|
|
14084
|
-
export const
|
|
14085
|
-
export const
|
|
14086
|
-
export const
|
|
14087
|
-
export const
|
|
14088
|
-
export const
|
|
14089
|
-
export const
|
|
14090
|
-
export const
|
|
14091
|
-
export const
|
|
14092
|
-
export const
|
|
14093
|
-
export const
|
|
14094
|
-
export const
|
|
14095
|
-
export const
|
|
14096
|
-
export const
|
|
14097
|
-
export const
|
|
14098
|
-
export const
|
|
14099
|
-
export const
|
|
14100
|
-
export const
|
|
14101
|
-
export const
|
|
14102
|
-
export const
|
|
14103
|
-
export const
|
|
14104
|
-
export const
|
|
14105
|
-
export const
|
|
14106
|
-
export const
|
|
14107
|
-
export const
|
|
14108
|
-
export const
|
|
14109
|
-
export const
|
|
14110
|
-
export const
|
|
14111
|
-
export const
|
|
14112
|
-
export const
|
|
14113
|
-
export const
|
|
14114
|
-
export const
|
|
14115
|
-
export const
|
|
14116
|
-
export const
|
|
14117
|
-
export const
|
|
14118
|
-
export const
|
|
14119
|
-
export const
|
|
14120
|
-
export const
|
|
14121
|
-
export const
|
|
14122
|
-
export const
|
|
14123
|
-
export const
|
|
14124
|
-
export const
|
|
14125
|
-
export const
|
|
14126
|
-
export const
|
|
14127
|
-
export const
|
|
14128
|
-
export const
|
|
14129
|
-
export const
|
|
14130
|
-
export const
|
|
14131
|
-
export const
|
|
14132
|
-
export const
|
|
14133
|
-
export const
|
|
14134
|
-
export const
|
|
14135
|
-
export const
|
|
14136
|
-
export const
|
|
14137
|
-
export const
|
|
14138
|
-
export const
|
|
14139
|
-
export const
|
|
14140
|
-
export const
|
|
14141
|
-
export const
|
|
14142
|
-
export const
|
|
14143
|
-
export const
|
|
14144
|
-
export const
|
|
14145
|
-
export const
|
|
14146
|
-
export const
|
|
14147
|
-
export const
|
|
14148
|
-
export const
|
|
14149
|
-
export const
|
|
14150
|
-
export const
|
|
14151
|
-
export const
|
|
14152
|
-
export const
|
|
14153
|
-
export const
|
|
14154
|
-
export const
|
|
14155
|
-
export const
|
|
14156
|
-
export const
|
|
14157
|
-
export const
|
|
14158
|
-
export const
|
|
14159
|
-
export const
|
|
14160
|
-
export const
|
|
14161
|
-
export const
|
|
14162
|
-
export const
|
|
14163
|
-
export const
|
|
14164
|
-
export const
|
|
14165
|
-
export const
|
|
14166
|
-
export const
|
|
14167
|
-
export const
|
|
14168
|
-
export const
|
|
14169
|
-
export const
|
|
14170
|
-
export const
|
|
14171
|
-
export const
|
|
14172
|
-
export const
|
|
14173
|
-
export const
|
|
14174
|
-
export const
|
|
14175
|
-
export const
|
|
14176
|
-
export const
|
|
14177
|
-
export const
|
|
14178
|
-
export const
|
|
14179
|
-
export const
|
|
14180
|
-
export const
|
|
14181
|
-
export const
|
|
14182
|
-
export const
|
|
14183
|
-
export const
|
|
14184
|
-
export const
|
|
14185
|
-
export const
|
|
14186
|
-
export const
|
|
14187
|
-
export const
|
|
14188
|
-
export const
|
|
14189
|
-
export const
|
|
14190
|
-
export const
|
|
14191
|
-
export const
|
|
14192
|
-
export const
|
|
14193
|
-
export const
|
|
14194
|
-
export const
|
|
14195
|
-
export const
|
|
14196
|
-
export const
|
|
14197
|
-
export const
|
|
14198
|
-
export const
|
|
14199
|
-
export const
|
|
14200
|
-
export const
|
|
14201
|
-
export const
|
|
14202
|
-
export const
|
|
14203
|
-
export const
|
|
14204
|
-
export const
|
|
14205
|
-
export const
|
|
14206
|
-
export const
|
|
14207
|
-
export const
|
|
14208
|
-
export const
|
|
14209
|
-
export const
|
|
14210
|
-
export const
|
|
14211
|
-
export const
|
|
14212
|
-
export const
|
|
14213
|
-
export const
|
|
14214
|
-
export const
|
|
14215
|
-
export const
|
|
14216
|
-
export const
|
|
14217
|
-
export const
|
|
14218
|
-
export const
|
|
14219
|
-
export const
|
|
14220
|
-
export const
|
|
14221
|
-
export const
|
|
14222
|
-
export const
|
|
14223
|
-
export const
|
|
14224
|
-
export const
|
|
14225
|
-
export const
|
|
14226
|
-
export const
|
|
14227
|
-
export const
|
|
14228
|
-
export const
|
|
14229
|
-
export const
|
|
14230
|
-
export const
|
|
14231
|
-
export const
|
|
14232
|
-
export const
|
|
14233
|
-
export const
|
|
14234
|
-
export const
|
|
14235
|
-
export const
|
|
14236
|
-
export const
|
|
14237
|
-
export const
|
|
14238
|
-
export const
|
|
14239
|
-
export const
|
|
14240
|
-
export const
|
|
14241
|
-
export const
|
|
14242
|
-
export const
|
|
14243
|
-
export const
|
|
14244
|
-
export const
|
|
14245
|
-
export const
|
|
14246
|
-
export const
|
|
14247
|
-
export const
|
|
14248
|
-
export const
|
|
14249
|
-
export const
|
|
14250
|
-
export const
|
|
14251
|
-
export const
|
|
14252
|
-
export const
|
|
14253
|
-
export const
|
|
14254
|
-
export const
|
|
14255
|
-
export const
|
|
14256
|
-
export const
|
|
14257
|
-
export const
|
|
14258
|
-
export const
|
|
14259
|
-
export const
|
|
14260
|
-
export const
|
|
14261
|
-
export const
|
|
14262
|
-
export const
|
|
14263
|
-
export const
|
|
14264
|
-
export const
|
|
14265
|
-
export const
|
|
14266
|
-
export const
|
|
14267
|
-
export const
|
|
14268
|
-
export const
|
|
14269
|
-
export const
|
|
14270
|
-
export const
|
|
14271
|
-
export const
|
|
14272
|
-
export const
|
|
14273
|
-
export const
|
|
14274
|
-
export const
|
|
15311
|
+
export const api8_reserveFailure = load("commands/bake/out-errors.js").reserveFailure;
|
|
15312
|
+
export const api8_OutputReserveError = load("commands/bake/out-errors.js").OutputReserveError;
|
|
15313
|
+
export const api9_reserveOut = load("commands/bake/write-out.js").reserveOut;
|
|
15314
|
+
export const api10_handlers = load("commands/handlers.js").handlers;
|
|
15315
|
+
export const api11_inspectResult = load("commands/inspect.js").inspectResult;
|
|
15316
|
+
export const api11_run = load("commands/inspect.js").run;
|
|
15317
|
+
export const api11_inspect = load("commands/inspect.js").inspect;
|
|
15318
|
+
export const api12_run = load("commands/inventory.js").run;
|
|
15319
|
+
export const api12_inventory = load("commands/inventory.js").inventory;
|
|
15320
|
+
export const api13_narrowLabel = load("commands/recommend.js").narrowLabel;
|
|
15321
|
+
export const api13_run = load("commands/recommend.js").run;
|
|
15322
|
+
export const api13_recommend = load("commands/recommend.js").recommend;
|
|
15323
|
+
export const api13_SHORT_OBSERVATION_SECONDS = load("commands/recommend.js").SHORT_OBSERVATION_SECONDS;
|
|
15324
|
+
export const api14_aggregateExit = load("commands/respond.js").aggregateExit;
|
|
15325
|
+
export const api14_run = load("commands/respond.js").run;
|
|
15326
|
+
export const api14_respond = load("commands/respond.js").respond;
|
|
15327
|
+
export const api14_ROOT_KEY_0_GUIDANCE = load("commands/respond.js").ROOT_KEY_0_GUIDANCE;
|
|
15328
|
+
export const api14_OWN_ON_ROOT_KEY_0 = load("commands/respond.js").OWN_ON_ROOT_KEY_0;
|
|
15329
|
+
export const api14_REVOKED_BUT_PRESENT_GUIDANCE = load("commands/respond.js").REVOKED_BUT_PRESENT_GUIDANCE;
|
|
15330
|
+
export const api14_BAKING_RISK_GUIDANCE = load("commands/respond.js").BAKING_RISK_GUIDANCE;
|
|
15331
|
+
export const api14_CUSTOM_CAVEAT_NOTE = load("commands/respond.js").CUSTOM_CAVEAT_NOTE;
|
|
15332
|
+
export const api14_UNKNOWN_GUIDANCE = load("commands/respond.js").UNKNOWN_GUIDANCE;
|
|
15333
|
+
export const api15_run = load("commands/revoke.js").run;
|
|
15334
|
+
export const api15_revoke = load("commands/revoke.js").revoke;
|
|
15335
|
+
export const api16_run = load("commands/rotate.js").run;
|
|
15336
|
+
export const api16_rotate = load("commands/rotate.js").rotate;
|
|
15337
|
+
export const api17_run = load("commands/scan.js").run;
|
|
15338
|
+
export const api18_run = load("commands/verify.js").run;
|
|
15339
|
+
export const api18_verify = load("commands/verify.js").verify;
|
|
15340
|
+
export const api18_ROOT_KEY_0_ACCEPTED = load("commands/verify.js").ROOT_KEY_0_ACCEPTED;
|
|
15341
|
+
export const api18_CAN_BAKE_ACCEPTED = load("commands/verify.js").CAN_BAKE_ACCEPTED;
|
|
15342
|
+
export const api18_CAN_BAKE_ROOT_KEY_0 = load("commands/verify.js").CAN_BAKE_ROOT_KEY_0;
|
|
15343
|
+
export const api18_OWN_ROOT_KEY_ACCEPTED = load("commands/verify.js").OWN_ROOT_KEY_ACCEPTED;
|
|
15344
|
+
export const api18_REJECTED_INCONCLUSIVE = load("commands/verify.js").REJECTED_INCONCLUSIVE;
|
|
15345
|
+
export const api18_LOCAL_EXPIRY_UNCONFIRMED = load("commands/verify.js").LOCAL_EXPIRY_UNCONFIRMED;
|
|
15346
|
+
export const api18_SIGNATURE_MISMATCH_INCONCLUSIVE = load("commands/verify.js").SIGNATURE_MISMATCH_INCONCLUSIVE;
|
|
15347
|
+
export const api18_SIGNATURE_MISMATCH_HOW_TO_CONCLUDE = load("commands/verify.js").SIGNATURE_MISMATCH_HOW_TO_CONCLUDE;
|
|
15348
|
+
export const api18_SIGNATURE_MISMATCH_ROOT_KEY_0 = load("commands/verify.js").SIGNATURE_MISMATCH_ROOT_KEY_0;
|
|
15349
|
+
export const api18_OWN_ROOT_KEY_INCONCLUSIVE = load("commands/verify.js").OWN_ROOT_KEY_INCONCLUSIVE;
|
|
15350
|
+
export const api18_ROOT_KEY_0_INCONCLUSIVE = load("commands/verify.js").ROOT_KEY_0_INCONCLUSIVE;
|
|
15351
|
+
export const api18_CUSTOM_CAVEAT_REJECTED = load("commands/verify.js").CUSTOM_CAVEAT_REJECTED;
|
|
15352
|
+
export const api19_scheduleDeadline = load("commands/watch.js").scheduleDeadline;
|
|
15353
|
+
export const api19_lockOwnerGone = load("commands/watch.js").lockOwnerGone;
|
|
15354
|
+
export const api19_acquireLock = load("commands/watch.js").acquireLock;
|
|
15355
|
+
export const api19_run = load("commands/watch.js").run;
|
|
15356
|
+
export const api19_stdoutEvents = load("commands/watch.js").stdoutEvents;
|
|
15357
|
+
export const api19_watch = load("commands/watch.js").watch;
|
|
15358
|
+
export const api19_CANARY_RELOAD_MS = load("commands/watch.js").CANARY_RELOAD_MS;
|
|
15359
|
+
export const api19_MAX_TIMER_MS = load("commands/watch.js").MAX_TIMER_MS;
|
|
15360
|
+
export const api20_redactEncodedCredentials = load("credential-recognition.js").redactEncodedCredentials;
|
|
15361
|
+
export const api21_macaroonEosEnd = load("decode/index.js").macaroonEosEnd;
|
|
15362
|
+
export const api21_macaroonPacketRange = load("decode/index.js").macaroonPacketRange;
|
|
15363
|
+
export const api21_macaroonSectionRange = load("decode/index.js").macaroonSectionRange;
|
|
15364
|
+
export const api21_parseTimeBefore = load("decode/index.js").parseTimeBefore;
|
|
15365
|
+
export const api21_canonicalMacaroonForm = load("decode/index.js").canonicalMacaroonForm;
|
|
15366
|
+
export const api21_decodeMacaroon = load("decode/index.js").decodeMacaroon;
|
|
15367
|
+
export const api21_macaroonId = load("decode/index.js").macaroonId;
|
|
15368
|
+
export const api21_encodeMacaroon = load("decode/index.js").encodeMacaroon;
|
|
15369
|
+
export const api21_addFirstPartyCaveat = load("decode/index.js").addFirstPartyCaveat;
|
|
15370
|
+
export const api21_timeBeforeCondition = load("decode/index.js").timeBeforeCondition;
|
|
15371
|
+
export const api21_validMacaroonIdentifier = load("decode/index.js").validMacaroonIdentifier;
|
|
15372
|
+
export const api21_isNwcContainer = load("decode/index.js").isNwcContainer;
|
|
15373
|
+
export const api21_decodeNwc = load("decode/index.js").decodeNwc;
|
|
15374
|
+
export const api21_DecodeError = load("decode/index.js").DecodeError;
|
|
15375
|
+
export const api22_macaroonEnds = load("decode/recognition.js").macaroonEnds;
|
|
15376
|
+
export const api23_unsafeNodeDiagnostics = load("environment.js").unsafeNodeDiagnostics;
|
|
15377
|
+
export const api24_UsageError = load("errors.js").UsageError;
|
|
15378
|
+
export const api24_InputError = load("errors.js").InputError;
|
|
15379
|
+
export const api24_DecodeError = load("errors.js").DecodeError;
|
|
15380
|
+
export const api24_LndError = load("errors.js").LndError;
|
|
15381
|
+
export const api24_LndUncertainError = load("errors.js").LndUncertainError;
|
|
15382
|
+
export const api25_grade = load("grade/index.js").grade;
|
|
15383
|
+
export const api25_gradeFacts = load("grade/index.js").gradeFacts;
|
|
15384
|
+
export const api25_describe = load("grade/index.js").describe;
|
|
15385
|
+
export const api25_canBake = load("grade/index.js").canBake;
|
|
15386
|
+
export const api25_BAKE_MACAROON_URI = load("grade/index.js").BAKE_MACAROON_URI;
|
|
15387
|
+
export const api26_groupedRuns = load("grouped-runs.js").groupedRuns;
|
|
15388
|
+
export const api26_groupedBase64 = load("grouped-runs.js").groupedBase64;
|
|
15389
|
+
export const api27_readCredential = load("input/index.js").readCredential;
|
|
15390
|
+
export const api27_InputError = load("input/index.js").InputError;
|
|
15391
|
+
export const api27_UsageError = load("input/index.js").UsageError;
|
|
15392
|
+
export const api28_parseLndconnect = load("input/lndconnect.js").parseLndconnect;
|
|
15393
|
+
export const api29_isInstant = load("instant.js").isInstant;
|
|
15394
|
+
export const api30_openGrpcStream = load("lnd/grpc.js").openGrpcStream;
|
|
15395
|
+
export const api30_MAX_GRPC_MESSAGE = load("lnd/grpc.js").MAX_GRPC_MESSAGE;
|
|
15396
|
+
export const api30_STALL_TIMEOUT_MS = load("lnd/grpc.js").STALL_TIMEOUT_MS;
|
|
15397
|
+
export const api30_MAX_PENDING_WRITE_BYTES = load("lnd/grpc.js").MAX_PENDING_WRITE_BYTES;
|
|
15398
|
+
export const api30_WRITE_OVERHEAD_BYTES = load("lnd/grpc.js").WRITE_OVERHEAD_BYTES;
|
|
15399
|
+
export const api30_PING_INTERVAL_MS = load("lnd/grpc.js").PING_INTERVAL_MS;
|
|
15400
|
+
export const api30_PING_TIMEOUT_MS = load("lnd/grpc.js").PING_TIMEOUT_MS;
|
|
15401
|
+
export const api31_createLndAdapter = load("lnd/index.js").createLndAdapter;
|
|
15402
|
+
export const api31_registerMiddleware = load("lnd/index.js").registerMiddleware;
|
|
15403
|
+
export const api32_decodeMiddlewareRequest = load("lnd/middleware-proto.js").decodeMiddlewareRequest;
|
|
15404
|
+
export const api32_encodeRegistration = load("lnd/middleware-proto.js").encodeRegistration;
|
|
15405
|
+
export const api32_encodeFeedback = load("lnd/middleware-proto.js").encodeFeedback;
|
|
15406
|
+
export const api32_MAX_MACAROON_BYTES = load("lnd/middleware-proto.js").MAX_MACAROON_BYTES;
|
|
15407
|
+
export const api32_MAX_METHOD_BYTES = load("lnd/middleware-proto.js").MAX_METHOD_BYTES;
|
|
15408
|
+
export const api32_MAX_CONDITION_BYTES = load("lnd/middleware-proto.js").MAX_CONDITION_BYTES;
|
|
15409
|
+
export const api32_UNKNOWN_METHOD = load("lnd/middleware-proto.js").UNKNOWN_METHOD;
|
|
15410
|
+
export const api33_identityCheck = load("lnd/tls-identity.js").identityCheck;
|
|
15411
|
+
export const api34_endpoint = load("online.js").endpoint;
|
|
15412
|
+
export const api34_credentialText = load("online.js").credentialText;
|
|
15413
|
+
export const api34_connectionProblem = load("online.js").connectionProblem;
|
|
15414
|
+
export const api34_connectionSettings = load("online.js").connectionSettings;
|
|
15415
|
+
export const api34_flagProblem = load("online.js").flagProblem;
|
|
15416
|
+
export const api34_nodeInfo = load("online.js").nodeInfo;
|
|
15417
|
+
export const api34_nodePubkey = load("online.js").nodePubkey;
|
|
15418
|
+
export const api34_drift = load("online.js").drift;
|
|
15419
|
+
export const api34_samePermissions = load("online.js").samePermissions;
|
|
15420
|
+
export const api34_listIds = load("online.js").listIds;
|
|
15421
|
+
export const api34_failureDetails = load("online.js").failureDetails;
|
|
15422
|
+
export const api34_onlineAdapter = load("online.js").onlineAdapter;
|
|
15423
|
+
export const api34_shellQuote = load("online.js").shellQuote;
|
|
15424
|
+
export const api34_recoveryPath = load("online.js").recoveryPath;
|
|
15425
|
+
export const api34_recoveryCommand = load("online.js").recoveryCommand;
|
|
15426
|
+
export const api34_inventoryRecoveryCommand = load("online.js").inventoryRecoveryCommand;
|
|
15427
|
+
export const api34_readOnlineCredential = load("online.js").readOnlineCredential;
|
|
15428
|
+
export const api34_onlineWarnings = load("online.js").onlineWarnings;
|
|
15429
|
+
export const api34_LABEL_RULE = load("online.js").LABEL_RULE;
|
|
15430
|
+
export const api34_readBoundedFile = load("online.js").readBoundedFile;
|
|
15431
|
+
export const api35_rotatePlanResult = load("output/index.js").rotatePlanResult;
|
|
15432
|
+
export const api35_revokePlanResult = load("output/index.js").revokePlanResult;
|
|
15433
|
+
export const api35_error = load("output/index.js").error;
|
|
15434
|
+
export const api35_warning = load("output/index.js").warning;
|
|
15435
|
+
export const api35_renderWatchEvent = load("output/index.js").renderWatchEvent;
|
|
15436
|
+
export const api35_formatDuration = load("output/index.js").formatDuration;
|
|
15437
|
+
export const api35_revokeConfirmation = load("output/index.js").revokeConfirmation;
|
|
15438
|
+
export const api35_writeRevokePrompt = load("output/index.js").writeRevokePrompt;
|
|
15439
|
+
export const api35_writeRotateFinishPlan = load("output/index.js").writeRotateFinishPlan;
|
|
15440
|
+
export const api35_rotateConfirmation = load("output/index.js").rotateConfirmation;
|
|
15441
|
+
export const api35_writeRotatePrompt = load("output/index.js").writeRotatePrompt;
|
|
15442
|
+
export const api35_render = load("output/index.js").render;
|
|
15443
|
+
export const api35_renderChunks = load("output/index.js").renderChunks;
|
|
15444
|
+
export const api35_inventoryResult = load("output/index.js").inventoryResult;
|
|
15445
|
+
export const api35_writeRotateStagePlan = load("output/index.js").writeRotateStagePlan;
|
|
15446
|
+
export const api36_ruleIdFor = load("output/scan-formats.js").ruleIdFor;
|
|
15447
|
+
export const api36_findingSeverity = load("output/scan-formats.js").findingSeverity;
|
|
15448
|
+
export const api36_locationOf = load("output/scan-formats.js").locationOf;
|
|
15449
|
+
export const api36_fileUri = load("output/scan-formats.js").fileUri;
|
|
15450
|
+
export const api36_sarifPieces = load("output/scan-formats.js").sarifPieces;
|
|
15451
|
+
export const api36_escapeData = load("output/scan-formats.js").escapeData;
|
|
15452
|
+
export const api36_escapeProperty = load("output/scan-formats.js").escapeProperty;
|
|
15453
|
+
export const api36_githubLines = load("output/scan-formats.js").githubLines;
|
|
15454
|
+
export const api36_SARIF_MAX_RESULTS = load("output/scan-formats.js").SARIF_MAX_RESULTS;
|
|
15455
|
+
export const api37_withProcessErrorContext = load("process-error-context.js").withProcessErrorContext;
|
|
15456
|
+
export const api37_markMutationSent = load("process-error-context.js").markMutationSent;
|
|
15457
|
+
export const api37_internalErrorMessage = load("process-error-context.js").internalErrorMessage;
|
|
15458
|
+
export const api38_isProtectedPath = load("protected-path.js").isProtectedPath;
|
|
15459
|
+
export const api38_outputNameProblem = load("protected-path.js").outputNameProblem;
|
|
15460
|
+
export const api38_resolveUnprotectedPath = load("protected-path.js").resolveUnprotectedPath;
|
|
15461
|
+
export const api39_openRegistry = load("registry/index.js").openRegistry;
|
|
15462
|
+
export const api39_replacementProblem = load("registry/index.js").replacementProblem;
|
|
15463
|
+
export const api39_replacementLinkProblems = load("registry/index.js").replacementLinkProblems;
|
|
15464
|
+
export const api39_recordedFileRelation = load("registry/index.js").recordedFileRelation;
|
|
15465
|
+
export const api39_isStagedReplacement = load("registry/index.js").isStagedReplacement;
|
|
15466
|
+
export const api39_hasCustomCaveat = load("registry/index.js").hasCustomCaveat;
|
|
15467
|
+
export const api39_RegistryInputError = load("registry/index.js").RegistryInputError;
|
|
15468
|
+
export const api39_RegistryUsageError = load("registry/index.js").RegistryUsageError;
|
|
15469
|
+
export const api39_Registry = load("registry/index.js").Registry;
|
|
15470
|
+
export const api40_validDomain = load("registry/process-domain.js").validDomain;
|
|
15471
|
+
export const api40_ownProcessDomain = load("registry/process-domain.js").ownProcessDomain;
|
|
15472
|
+
export const api41_buildAccessReview = load("report/access-review.js").buildAccessReview;
|
|
15473
|
+
export const api41_csvField = load("report/access-review.js").csvField;
|
|
15474
|
+
export const api41_formatAccessReview = load("report/access-review.js").formatAccessReview;
|
|
15475
|
+
export const api41_REPORT_FORMATS = load("report/access-review.js").REPORT_FORMATS;
|
|
15476
|
+
export const api41_LIMITATIONS = load("report/access-review.js").LIMITATIONS;
|
|
15477
|
+
export const api42_isRootKeyId = load("root-key-id.js").isRootKeyId;
|
|
15478
|
+
export const api42_MAX_ROOT_KEY_ID = load("root-key-id.js").MAX_ROOT_KEY_ID;
|
|
15479
|
+
export const api43_agentConfigCandidates = load("scan/agent-configs.js").agentConfigCandidates;
|
|
15480
|
+
export const api43_discoverAgentConfigs = load("scan/agent-configs.js").discoverAgentConfigs;
|
|
15481
|
+
export const api43_defaultAgentConfigEnvironment = load("scan/agent-configs.js").defaultAgentConfigEnvironment;
|
|
15482
|
+
export const api44_nonLocalPath = load("scan/git-history.js").nonLocalPath;
|
|
15483
|
+
export const api44_locateRepository = load("scan/git-history.js").locateRepository;
|
|
15484
|
+
export const api44_gitEnvironment = load("scan/git-history.js").gitEnvironment;
|
|
15485
|
+
export const api44_assertWorkTree = load("scan/git-history.js").assertWorkTree;
|
|
15486
|
+
export const api44_isShallowRepository = load("scan/git-history.js").isShallowRepository;
|
|
15487
|
+
export const api44_historyEntries = load("scan/git-history.js").historyEntries;
|
|
15488
|
+
export const api44_blobSizes = load("scan/git-history.js").blobSizes;
|
|
15489
|
+
export const api44_readBlobs = load("scan/git-history.js").readBlobs;
|
|
15490
|
+
export const api44_GitHistoryError = load("scan/git-history.js").GitHistoryError;
|
|
15491
|
+
export const api44_MODE_REGULAR = load("scan/git-history.js").MODE_REGULAR;
|
|
15492
|
+
export const api44_MODE_SYMLINK = load("scan/git-history.js").MODE_SYMLINK;
|
|
15493
|
+
export const api44_MODE_OTHER = load("scan/git-history.js").MODE_OTHER;
|
|
15494
|
+
export const api44_GIT_IDLE_TIMEOUT_MS = load("scan/git-history.js").GIT_IDLE_TIMEOUT_MS;
|
|
15495
|
+
export const api44_MAX_HISTORY_ENTRIES = load("scan/git-history.js").MAX_HISTORY_ENTRIES;
|
|
15496
|
+
export const api44_PINNED_CONFIG = load("scan/git-history.js").PINNED_CONFIG;
|
|
15497
|
+
export const api44_PREFLIGHT_LIMITS = load("scan/git-history.js").PREFLIGHT_LIMITS;
|
|
15498
|
+
export const api45_parseIgnoreFile = load("scan/ignore.js").parseIgnoreFile;
|
|
15499
|
+
export const api45_IgnoreFileError = load("scan/ignore.js").IgnoreFileError;
|
|
15500
|
+
export const api46_scan = load("scan/index.js").scan;
|
|
15501
|
+
export const api46_IgnoreFileError = load("scan/index.js").IgnoreFileError;
|
|
15502
|
+
export const api46_GitHistoryError = load("scan/index.js").GitHistoryError;
|
|
15503
|
+
export const api46_CANDIDATE_LIMIT = load("scan/index.js").CANDIDATE_LIMIT;
|
|
15504
|
+
export const api46_FINDINGS_LIMIT = load("scan/index.js").FINDINGS_LIMIT;
|
|
15505
|
+
export const api46_ALSO_AT_EXAMPLES = load("scan/index.js").ALSO_AT_EXAMPLES;
|
|
15506
|
+
export const api46_HISTORY_DEDUP_WORK_LIMIT = load("scan/index.js").HISTORY_DEDUP_WORK_LIMIT;
|
|
15507
|
+
export const api46_FindingsLimitError = load("scan/index.js").FindingsLimitError;
|
|
15508
|
+
export const api46_SCAN_CONCURRENCY = load("scan/index.js").SCAN_CONCURRENCY;
|
|
15509
|
+
export const api46_readBounded = load("scan/index.js").readBounded;
|
|
15510
|
+
export const api46_SCAN_OPEN_FLAGS = load("scan/index.js").SCAN_OPEN_FLAGS;
|
|
15511
|
+
export const api47_mcpServerStringSpans = load("scan/mcp-context.js").mcpServerStringSpans;
|
|
15512
|
+
export const api47_withinSpans = load("scan/mcp-context.js").withinSpans;
|
|
15513
|
+
export const api47_MAX_DEPTH = load("scan/mcp-context.js").MAX_DEPTH;
|
|
15514
|
+
export const api48_nwcMatches = load("scan/nwc-match.js").nwcMatches;
|
|
15515
|
+
export const api48_replaceNwc = load("scan/nwc-match.js").replaceNwc;
|
|
15516
|
+
export const api48_nwcUnescaped = load("scan/nwc-match.js").nwcUnescaped;
|
|
15517
|
+
export const api49_macaroonHex = load("scan/patterns.js").macaroonHex;
|
|
15518
|
+
export const api49_macaroonBase64 = load("scan/patterns.js").macaroonBase64;
|
|
15519
|
+
export const api49_lndconnect = load("scan/patterns.js").lndconnect;
|
|
15520
|
+
export const api49_nwc = load("scan/patterns.js").nwc;
|
|
15521
|
+
export const api50_redactDecoded = load("scan/redact.js").redactDecoded;
|
|
15522
|
+
export const api50_redactPath = load("scan/redact.js").redactPath;
|
|
15523
|
+
export const api50_collectNwcSecrets = load("scan/redact.js").collectNwcSecrets;
|
|
15524
|
+
export const api50_knownSecretRedactor = load("scan/redact.js").knownSecretRedactor;
|
|
15525
|
+
export const api50_redactKnownSecrets = load("scan/redact.js").redactKnownSecrets;
|
|
15526
|
+
export const api51_Secret = load("secret.js").Secret;
|
|
15527
|
+
export const api52_macwardenVersion = load("version.js").macwardenVersion;
|
|
15528
|
+
export const api53_createAlertThrottle = load("watch/alert-throttle.js").createAlertThrottle;
|
|
15529
|
+
export const api53_createAlertRunner = load("watch/alert-throttle.js").createAlertRunner;
|
|
15530
|
+
export const api53_ALERT_WINDOW_MS = load("watch/alert-throttle.js").ALERT_WINDOW_MS;
|
|
15531
|
+
export const api53_MAX_TRACKED_CANARIES = load("watch/alert-throttle.js").MAX_TRACKED_CANARIES;
|
|
15532
|
+
export const api54_reapAlertGroups = load("watch/alert.js").reapAlertGroups;
|
|
15533
|
+
export const api54_alertCleanupDone = load("watch/alert.js").alertCleanupDone;
|
|
15534
|
+
export const api54_runAlertCommand = load("watch/alert.js").runAlertCommand;
|
|
15535
|
+
export const api54_ALERT_TIMEOUT_MS = load("watch/alert.js").ALERT_TIMEOUT_MS;
|
|
15536
|
+
export const api54_ALERT_KILL_MS = load("watch/alert.js").ALERT_KILL_MS;
|
|
15537
|
+
export const api54_ALERT_GRACE_MS = load("watch/alert.js").ALERT_GRACE_MS;
|
|
15538
|
+
export const api54_ALERT_CLEANUP_MS = load("watch/alert.js").ALERT_CLEANUP_MS;
|
|
15539
|
+
export const api55_canaryConfigPath = load("watch/canary.js").canaryConfigPath;
|
|
15540
|
+
export const api55_newCaveatName = load("watch/canary.js").newCaveatName;
|
|
15541
|
+
export const api55_newCondition = load("watch/canary.js").newCondition;
|
|
15542
|
+
export const api55_canaryCondition = load("watch/canary.js").canaryCondition;
|
|
15543
|
+
export const api55_readCanaryConfig = load("watch/canary.js").readCanaryConfig;
|
|
15544
|
+
export const api55_ensureCanaryConfig = load("watch/canary.js").ensureCanaryConfig;
|
|
15545
|
+
export const api55_canaryEntries = load("watch/canary.js").canaryEntries;
|
|
15546
|
+
export const api55_isCanary = load("watch/canary.js").isCanary;
|
|
15547
|
+
export const api55_CANARY_FILE_LIMIT = load("watch/canary.js").CANARY_FILE_LIMIT;
|
|
15548
|
+
export const api56_retryable = load("watch/output-queue.js").retryable;
|
|
15549
|
+
export const api56_fdWriter = load("watch/output-queue.js").fdWriter;
|
|
15550
|
+
export const api56_createOutputQueue = load("watch/output-queue.js").createOutputQueue;
|
|
15551
|
+
export const api56_OUTPUT_LIMIT_BYTES = load("watch/output-queue.js").OUTPUT_LIMIT_BYTES;
|
|
15552
|
+
export const api57_usagePath = load("watch/usage.js").usagePath;
|
|
15553
|
+
export const api57_readUsage = load("watch/usage.js").readUsage;
|
|
15554
|
+
export const api57_writeUsage = load("watch/usage.js").writeUsage;
|
|
15555
|
+
export const api57_USAGE_LIMIT = load("watch/usage.js").USAGE_LIMIT;
|
|
15556
|
+
export const api57_CANARY_LIMIT = load("watch/usage.js").CANARY_LIMIT;
|
|
15557
|
+
export const api57_COVERAGE_LIMIT = load("watch/usage.js").COVERAGE_LIMIT;
|
|
15558
|
+
export const api57_SPAN_LIMIT = load("watch/usage.js").SPAN_LIMIT;
|
|
15559
|
+
export const api57_USAGE_FILE_LIMIT = load("watch/usage.js").USAGE_FILE_LIMIT;
|
|
15560
|
+
export const api57_UsageFileError = load("watch/usage.js").UsageFileError;
|
|
15561
|
+
export const api57_UsageStore = load("watch/usage.js").UsageStore;
|