@ai-ecoverse/slicc-kernel 1.176.2 → 1.177.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2796,8 +2796,8 @@ var OpfsFs = class {
2796
2796
  const [entry] = await this.meta.get([target]);
2797
2797
  if (entry?.fifo || entry?.socket) throw fsError("ENXIO", path);
2798
2798
  const kept = fresh3 ? cached : this.files.get(target);
2799
- const usable2 = kept && performance.now() - (this.found.get(kept) ?? -CHECK_MS) < CHECK_MS;
2800
- return [usable2 ? kept : void 0, target, true];
2799
+ const usable3 = kept && performance.now() - (this.found.get(kept) ?? -CHECK_MS) < CHECK_MS;
2800
+ return [usable3 ? kept : void 0, target, true];
2801
2801
  }
2802
2802
  async writeAt(handle, target, path, offset, bytes) {
2803
2803
  await this.counting(target);
@@ -8774,7 +8774,7 @@ function satisfies(version, range2) {
8774
8774
  }
8775
8775
 
8776
8776
  // src/version.ts
8777
- var KERNEL_VERSION = true ? "1.176.2" : "0.0.0-dev";
8777
+ var KERNEL_VERSION = true ? "1.177.0" : "0.0.0-dev";
8778
8778
  var OS_RELEASE = "6.0.0-slicc";
8779
8779
  function procVersion() {
8780
8780
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -18801,8 +18801,8 @@ var SCRUB_IDLE_MS = 50;
18801
18801
  var SCRUB_NONBLOCK_MAX = 64 * 1024;
18802
18802
  var encoder5 = new TextEncoder();
18803
18803
  function scrubberOf(secrets, sameLength = false) {
18804
- const usable2 = secrets.filter((s) => encoder5.encode(s.value).length >= MIN_VALUE);
18805
- return usable2.length > 0 ? compile(usable2, { idleMs: SCRUB_IDLE_MS, sameLength }) : void 0;
18804
+ const usable3 = secrets.filter((s) => encoder5.encode(s.value).length >= MIN_VALUE);
18805
+ return usable3.length > 0 ? compile(usable3, { idleMs: SCRUB_IDLE_MS, sameLength }) : void 0;
18806
18806
  }
18807
18807
  async function writeAll2(file, bytes) {
18808
18808
  const write = file.write;
@@ -19441,6 +19441,56 @@ var SealedBroker = class {
19441
19441
  }
19442
19442
  };
19443
19443
 
19444
+ // src/sudo-timestamp.ts
19445
+ var TIMESTAMPS_MAX = 64;
19446
+ var MINUTE_MS2 = 6e4;
19447
+ function timestampKey(owner, type) {
19448
+ if (type === "global") return "global";
19449
+ const { terminal, parent } = owner;
19450
+ if (type === "tty" && terminal) {
19451
+ return JSON.stringify(["tty", terminal.tty, terminal.sid, terminal.started]);
19452
+ }
19453
+ return parent ? JSON.stringify(["ppid", parent.pid, parent.started]) : void 0;
19454
+ }
19455
+ var Timestamps = class {
19456
+ users = /* @__PURE__ */ new Map();
19457
+ now;
19458
+ constructor(now) {
19459
+ this.now = now;
19460
+ }
19461
+ current(owner, policy) {
19462
+ if (policy.minutes === 0) return false;
19463
+ const key = timestampKey(owner, policy.type);
19464
+ const at = key === void 0 ? void 0 : this.users.get(owner.uid)?.get(key);
19465
+ if (at === void 0) return false;
19466
+ const now = this.now();
19467
+ return at <= now && (policy.minutes < 0 || now - at < policy.minutes * MINUTE_MS2);
19468
+ }
19469
+ stamp(owner, policy) {
19470
+ const key = policy.minutes === 0 ? void 0 : timestampKey(owner, policy.type);
19471
+ if (key === void 0) return false;
19472
+ const now = this.now();
19473
+ const records = this.users.get(owner.uid) ?? /* @__PURE__ */ new Map();
19474
+ this.users.set(owner.uid, records);
19475
+ records.delete(key);
19476
+ records.set(key, now);
19477
+ for (const [old, at] of records) {
19478
+ const stale = policy.minutes > 0 && (at > now || now - at >= policy.minutes * MINUTE_MS2);
19479
+ if (stale || records.size > TIMESTAMPS_MAX) records.delete(old);
19480
+ }
19481
+ return true;
19482
+ }
19483
+ invalidate(owner, type) {
19484
+ const key = timestampKey(owner, type);
19485
+ const records = this.users.get(owner.uid);
19486
+ if (key !== void 0) records?.delete(key);
19487
+ if (records?.size === 0) this.users.delete(owner.uid);
19488
+ }
19489
+ remove(uid) {
19490
+ this.users.delete(uid);
19491
+ }
19492
+ };
19493
+
19444
19494
  // src/sudoers.ts
19445
19495
  var SUDOERS = "/etc/sudoers";
19446
19496
  var SUDOERS_D = "/etc/sudoers.d";
@@ -19460,6 +19510,8 @@ var SHOWN_MAX = 60;
19460
19510
  var KERNEL_SYNTAX = "[NOPASSWD] <user|%group|ALL> [(<target>)] [Cwd <dir>] [Stdin none] Cmnd <command\u2026|*>";
19461
19511
  var SUDOERS_SYNTAX = "<user|%group|ALL> ALL=[(<target>[,<target>\u2026])] [NOPASSWD:] <command\u2026|ALL>";
19462
19512
  var EXPECTED = `expected '${KERNEL_SYNTAX}' or '${SUDOERS_SYNTAX}'`;
19513
+ var TIMESTAMP_TIMEOUT = 5;
19514
+ var TIMESTAMP_TYPE = "tty";
19463
19515
  function hasControl(text) {
19464
19516
  return CONTROL.test(text);
19465
19517
  }
@@ -19742,9 +19794,6 @@ function directiveError(text) {
19742
19794
  return /^#-?\d+\s+\S+\s*=/.test(text) ? "a #uid isn't supported: name the user, %group or ALL" : null;
19743
19795
  }
19744
19796
  const first = /^\S+/.exec(text)?.[0] ?? "";
19745
- if (/^Defaults([:@>!]|$)/.test(first)) {
19746
- return "Defaults aren't supported: the kernel's sudo has no settings, so this changes nothing";
19747
- }
19748
19797
  if (/^[A-Z][A-Za-z]*_Alias$/.test(first)) {
19749
19798
  return "aliases aren't supported: name the users, targets and commands in each line";
19750
19799
  }
@@ -19770,9 +19819,90 @@ function kernelLine(m, line, file) {
19770
19819
  )
19771
19820
  };
19772
19821
  }
19822
+ var DEFAULTS = /^Defaults(?=[:@>!\s]|$)/;
19823
+ var DEFAULTS_ONLY = `Defaults aren't supported, except timestamp_timeout and timestamp_type for everyone, a user or a %group: this line changes nothing`;
19824
+ var PARAM = /^(!*)([A-Za-z_]+)(?:\s*([-+]?=)\s*(.*))?$/s;
19825
+ var MINUTES = /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)$/;
19826
+ var TYPES = ["tty", "ppid", "global"];
19827
+ function defaultsValue(raw2) {
19828
+ const quoted = /^"(.*)"$/s.exec(raw2);
19829
+ return quoted ? quoted[1] : raw2;
19830
+ }
19831
+ function timeoutParam({ text, negated, op, value: value2 }) {
19832
+ if (negated && op === void 0) return { timestampTimeout: 0 };
19833
+ if (negated || op !== "=" || !MINUTES.test(value2) || !Number.isFinite(Number(value2))) {
19834
+ return `timestamp_timeout takes minutes, such as =5 or =2.5 (0 always asks, below 0 never expires), not ${shown(text)}`;
19835
+ }
19836
+ return { timestampTimeout: Number(value2) };
19837
+ }
19838
+ function typeParam({ text, negated, op, value: value2 }) {
19839
+ if (!negated && op === "=" && TYPES.includes(value2)) {
19840
+ return { timestampType: value2 };
19841
+ }
19842
+ if (value2 === "kernel") return "timestamp_type=kernel isn't supported: use tty, ppid or global";
19843
+ return `timestamp_type takes =tty, =ppid or =global, not ${shown(text)}`;
19844
+ }
19845
+ function defaultsParam(param) {
19846
+ const m = PARAM.exec(param);
19847
+ if (!m) return DEFAULTS_ONLY;
19848
+ const [, bangs = "", name2, op, raw2 = ""] = m;
19849
+ const read = { text: param, negated: bangs !== "", op, value: defaultsValue(raw2.trim()) };
19850
+ const settings = name2 === "timestamp_timeout" ? timeoutParam(read) : name2 === "timestamp_type" ? typeParam(read) : DEFAULTS_ONLY;
19851
+ if (typeof settings === "string") return settings;
19852
+ const value2 = settings.timestampTimeout ?? settings.timestampType;
19853
+ return { settings, text: `${name2}=${value2}` };
19854
+ }
19855
+ function defaultsWho(binding) {
19856
+ if (binding === "") return null;
19857
+ if (!binding.startsWith(":")) return DEFAULTS_ONLY;
19858
+ const who = binding.slice(1).split(",");
19859
+ for (const name2 of who) {
19860
+ if (!validWho(name2))
19861
+ return `expected users, %groups or ALL after Defaults:, not ${shown(name2)}`;
19862
+ }
19863
+ return who;
19864
+ }
19865
+ function defaultsLine(text) {
19866
+ const m = /^Defaults(\S*)(?:\s+(.*))?$/s.exec(text);
19867
+ const who = defaultsWho(m?.[1] ?? "");
19868
+ if (typeof who === "string") return { error: who };
19869
+ const list = (m?.[2] ?? "").trim();
19870
+ if (list === "")
19871
+ return {
19872
+ error: "expected timestamp_timeout=<minutes> or timestamp_type=<type> after Defaults"
19873
+ };
19874
+ const settings = {};
19875
+ const params = [];
19876
+ for (const param of list.split(",")) {
19877
+ const read = defaultsParam(param.trim());
19878
+ if (typeof read === "string") return { error: read };
19879
+ Object.assign(settings, read.settings);
19880
+ params.push(read.text);
19881
+ }
19882
+ return { defaults: { who, settings, params } };
19883
+ }
19884
+ function resolveDefaults(entries, applies) {
19885
+ const everyone = entries.filter((e) => e.who === null);
19886
+ const mine = entries.filter((e) => e.who?.some(applies));
19887
+ const matching = [...everyone, ...mine];
19888
+ const settings = Object.assign({}, ...matching.map((e) => e.settings));
19889
+ return {
19890
+ timestampTimeout: settings.timestampTimeout ?? TIMESTAMP_TIMEOUT,
19891
+ timestampType: settings.timestampType ?? TIMESTAMP_TYPE,
19892
+ matching
19893
+ };
19894
+ }
19895
+ function defaultsText(entries) {
19896
+ const all = resolveDefaults(entries, () => false);
19897
+ return [
19898
+ `Defaults timestamp_timeout=${all.timestampTimeout}, timestamp_type=${all.timestampType}`,
19899
+ ...entries.filter((e) => e.who !== null).map((e) => `Defaults:${e.who.join(",")} ${e.params.join(", ")}`)
19900
+ ];
19901
+ }
19773
19902
  function parseLine(line, file) {
19774
19903
  const text = line.trim();
19775
19904
  if (text === "") return null;
19905
+ if (DEFAULTS.test(text)) return defaultsLine(text);
19776
19906
  const directive = directiveError(text);
19777
19907
  if (directive !== void 0) return directive === null ? null : { error: directive };
19778
19908
  const kernel = KERNEL_FORM.exec(text);
@@ -19784,14 +19914,16 @@ function parseLine(line, file) {
19784
19914
  }
19785
19915
  function parsePolicy(text, file) {
19786
19916
  const rules = [];
19917
+ const defaults2 = [];
19787
19918
  const errors = [];
19788
19919
  for (const [n, line] of text.split("\n").entries()) {
19789
19920
  const parsed2 = parseLine(line, file);
19790
19921
  if (parsed2 === null) continue;
19791
19922
  if ("error" in parsed2) errors.push({ line: n + 1, message: parsed2.error });
19923
+ else if ("defaults" in parsed2) defaults2.push(parsed2.defaults);
19792
19924
  else rules.push(...parsed2.rules);
19793
19925
  }
19794
- return { rules, errors };
19926
+ return { rules, defaults: defaults2, errors };
19795
19927
  }
19796
19928
 
19797
19929
  // src/sudo.ts
@@ -19799,7 +19931,14 @@ var SECURE_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
19799
19931
  var DEFAULT_SUDOERS = "%sudo Cmnd *\n";
19800
19932
  var SUDO_TIMEOUT_MS = 3e5;
19801
19933
  var TERM = /^[A-Za-z0-9._+-]+$/;
19802
- var USAGE = "usage: sudo [-n] [-u user] [--] command [args...]\n sudo -l";
19934
+ var USAGE = [
19935
+ "usage: sudo -K | -k",
19936
+ " sudo -v [-kn] [-u user]",
19937
+ " sudo -l [-kn]",
19938
+ " sudo [-kn] [-u user] [--] command [args...]"
19939
+ ].join("\n");
19940
+ var DONE = { done: true };
19941
+ var NOT_STAMPED = "sudo: approved, but the approver didn't say a person decided, so no timestamp was recorded";
19803
19942
  var DENIED2 = "sudo: approval denied";
19804
19943
  var DENIAL_MAX = 500;
19805
19944
  var ABORTED = /* @__PURE__ */ Symbol("aborted");
@@ -19873,6 +20012,17 @@ async function answerOf(approve, req, signal) {
19873
20012
  return cut === void 0 ? "deny" : await cut;
19874
20013
  }
19875
20014
  }
20015
+ function approvalOf(answer2) {
20016
+ if (answer2 === "once" || answer2 === "always") return { answer: answer2, person: false };
20017
+ if (typeof answer2 !== "object" || answer2 === null) return void 0;
20018
+ try {
20019
+ const { answer: kind, timestamp } = answer2;
20020
+ if (kind !== "once" && kind !== "always" || timestamp !== true) return void 0;
20021
+ return { answer: kind, person: true };
20022
+ } catch {
20023
+ return void 0;
20024
+ }
20025
+ }
19876
20026
  function recordedCommand(argv) {
19877
20027
  const [path = "", ...args] = argv;
19878
20028
  return commandText({ path, args: args.map((a) => [a]) });
@@ -19882,6 +20032,9 @@ function flags(parsed2, arg, next3) {
19882
20032
  const flag = arg[j];
19883
20033
  if (flag === "n") parsed2.nonInteractive = true;
19884
20034
  else if (flag === "l") parsed2.list = true;
20035
+ else if (flag === "v") parsed2.validate = true;
20036
+ else if (flag === "k") parsed2.reset = true;
20037
+ else if (flag === "K") parsed2.removeAll = true;
19885
20038
  else if (flag !== "u") return `sudo: invalid option -- '${flag}'
19886
20039
  ${USAGE}`;
19887
20040
  else {
@@ -19895,7 +20048,15 @@ ${USAGE}`;
19895
20048
  return 0;
19896
20049
  }
19897
20050
  function parseArgs(args) {
19898
- const parsed2 = { list: false, nonInteractive: false, user: "root", command: [] };
20051
+ const parsed2 = {
20052
+ list: false,
20053
+ nonInteractive: false,
20054
+ validate: false,
20055
+ reset: false,
20056
+ removeAll: false,
20057
+ user: "root",
20058
+ command: []
20059
+ };
19899
20060
  let i = 0;
19900
20061
  for (; i < args.length; i++) {
19901
20062
  const arg = args[i];
@@ -19909,8 +20070,17 @@ function parseArgs(args) {
19909
20070
  i += used2;
19910
20071
  }
19911
20072
  parsed2.command = args.slice(i);
19912
- if (parsed2.list ? parsed2.command.length > 0 : parsed2.command.length === 0) return USAGE;
19913
- return parsed2;
20073
+ return usable(parsed2) ? parsed2 : USAGE;
20074
+ }
20075
+ function usable(parsed2) {
20076
+ const command = parsed2.command.length > 0;
20077
+ const modes = [parsed2.list, parsed2.validate, parsed2.removeAll].filter(Boolean).length;
20078
+ if (modes > 1 || parsed2.removeAll && parsed2.reset) return false;
20079
+ if (modes === 1) return !command;
20080
+ return command || parsed2.reset;
20081
+ }
20082
+ function policyOf(settings) {
20083
+ return { type: settings.timestampType, minutes: settings.timestampTimeout };
19914
20084
  }
19915
20085
  function described(rule) {
19916
20086
  const cmnd = rule.cmnd === "*" ? "ALL" : commandText(rule.cmnd);
@@ -19922,9 +20092,11 @@ var Sudo = class {
19922
20092
  o;
19923
20093
  warned = /* @__PURE__ */ new Set();
19924
20094
  pending = /* @__PURE__ */ new Set();
20095
+ timestamps;
19925
20096
  next = 0;
19926
20097
  constructor(options) {
19927
20098
  this.o = options;
20099
+ this.timestamps = new Timestamps(options.now ?? (() => performance.now()));
19928
20100
  }
19929
20101
  warn(message3) {
19930
20102
  if (this.warned.has(message3)) return;
@@ -20011,8 +20183,8 @@ var Sudo = class {
20011
20183
  const code = err.code;
20012
20184
  return skipped(`it can't be read (${typeof code === "string" ? code : "error"})`);
20013
20185
  }
20014
- const { rules, errors } = parsePolicy(text, path);
20015
- return { path, rules, problems: errors, ok: true };
20186
+ const { rules, defaults: defaults2, errors } = parsePolicy(text, path);
20187
+ return { path, rules, defaults: defaults2, problems: errors, ok: true };
20016
20188
  }
20017
20189
  async policy() {
20018
20190
  const files = [await this.policyFile(SUDOERS)];
@@ -20041,7 +20213,10 @@ var Sudo = class {
20041
20213
  }
20042
20214
  }
20043
20215
  if (!detailed && skipped > 0) await caller.report?.(skippedNotice(skipped));
20044
- return files.flatMap((f) => f.rules);
20216
+ return {
20217
+ rules: files.flatMap((f) => f.rules),
20218
+ defaults: files.flatMap((f) => f.defaults ?? [])
20219
+ };
20045
20220
  }
20046
20221
  async check(cred, call) {
20047
20222
  const lines2 = [];
@@ -20054,17 +20229,26 @@ var Sudo = class {
20054
20229
  if (read && problems.length === 0) lines2.push([1, `${path}: parsed OK`]);
20055
20230
  if (problems.length > 0) ok4 = false;
20056
20231
  };
20232
+ const values = (defaults2) => {
20233
+ for (const line of defaultsText(defaults2)) lines2.push([1, line]);
20234
+ };
20057
20235
  if (call.files !== void 0) {
20236
+ const defaults2 = [];
20058
20237
  for (const { name: name2, text } of call.files) {
20059
- report({ path: name2, rules: [], problems: parsePolicy(text).errors, ok: true });
20238
+ const parsed2 = parsePolicy(text);
20239
+ defaults2.push(...parsed2.defaults);
20240
+ report({ path: name2, rules: [], problems: parsed2.errors, ok: true });
20060
20241
  }
20242
+ if (defaults2.length > 0) values(defaults2);
20061
20243
  return { lines: lines2, ok: ok4 };
20062
20244
  }
20063
20245
  if (this.o.mode === "enforce" && cred.euid !== 0) {
20064
20246
  const hint = "check the policy as root (sudo visudo -c), or a file with visudo -c -f <file>";
20065
20247
  return { lines: [[2, `visudo: ${SUDOERS}: Permission denied: ${hint}`]], ok: false };
20066
20248
  }
20067
- for (const file of await this.policy()) report(file);
20249
+ const files = await this.policy();
20250
+ for (const file of files) report(file);
20251
+ values(files.flatMap((f) => f.defaults ?? []));
20068
20252
  return { lines: lines2, ok: ok4 };
20069
20253
  }
20070
20254
  async groupIds() {
@@ -20073,9 +20257,9 @@ var Sudo = class {
20073
20257
  async applies(caller, cred) {
20074
20258
  const gids = await this.groupIds();
20075
20259
  const mine = /* @__PURE__ */ new Set([cred.rgid, cred.egid, ...cred.groups]);
20076
- return (rule) => {
20077
- if (rule.who === "ALL" || rule.who === caller.name) return true;
20078
- const gid = rule.who.startsWith("%") ? gids.get(rule.who.slice(1)) : void 0;
20260
+ return (who) => {
20261
+ if (who === "ALL" || who === caller.name) return true;
20262
+ const gid = who.startsWith("%") ? gids.get(who.slice(1)) : void 0;
20079
20263
  return gid !== void 0 && mine.has(gid);
20080
20264
  };
20081
20265
  }
@@ -20120,18 +20304,40 @@ var Sudo = class {
20120
20304
  const mode = this.o.mode;
20121
20305
  if (parsed2.list) return { list: `sudo: users are '${mode}': commands run as the caller
20122
20306
  ` };
20123
- return { run: parsed2.command };
20307
+ return parsed2.command.length > 0 ? { run: parsed2.command } : DONE;
20124
20308
  }
20125
20309
  const { cred } = caller;
20126
20310
  const policy = await this.policyRules(caller);
20127
20311
  const self = await this.o.users.lookup(cred.ruid);
20128
20312
  if (!self) return failed2("EPERM", `sudo: uid ${cred.ruid} is not in the passwd database`);
20313
+ const owner = {
20314
+ uid: cred.ruid,
20315
+ terminal: caller.terminal,
20316
+ parent: caller.parent
20317
+ };
20318
+ const applies = await this.applies(self, cred);
20319
+ const settings = resolveDefaults(policy.defaults, applies);
20320
+ if (parsed2.removeAll) {
20321
+ this.timestamps.remove(owner.uid);
20322
+ return DONE;
20323
+ }
20324
+ if (parsed2.reset && !parsed2.validate && !parsed2.list && parsed2.command.length === 0) {
20325
+ this.timestamps.invalidate(owner, settings.timestampType);
20326
+ return DONE;
20327
+ }
20129
20328
  const target = await this.lookup(parsed2.user);
20130
20329
  if (!target) return failed2("ENOENT", `sudo: unknown user ${parsed2.user}`);
20131
20330
  const root = cred.euid === 0;
20132
- const rules = root ? [] : policy;
20133
- const applies = await this.applies(self, cred);
20134
- if (parsed2.list) return this.listed(self, root, rules.filter(applies));
20331
+ const rules = root ? [] : policy.rules;
20332
+ const mine = rules.filter((rule) => applies(rule.who));
20333
+ if (parsed2.list) return this.listed(self, root, mine, settings.matching);
20334
+ const session = { caller, self, target, owner, policy: policyOf(settings), parsed: parsed2, signal };
20335
+ if (parsed2.validate) return this.validate(session, root, mine, call.cwd);
20336
+ return this.command(session, root, mine, call);
20337
+ }
20338
+ async command(s, root, mine, call) {
20339
+ const { caller, self, target, parsed: parsed2 } = s;
20340
+ const { cred } = caller;
20135
20341
  const [word = "", ...args] = parsed2.command;
20136
20342
  const cwd = await this.canonical(call.cwd);
20137
20343
  if (cwd === void 0 || !await this.reachable(cred, cwd)) {
@@ -20145,8 +20351,8 @@ var Sudo = class {
20145
20351
  const untrusted2 = await this.untrusted(path, cwd, word);
20146
20352
  if (untrusted2) return untrusted2;
20147
20353
  const argv = [path, ...args];
20148
- const matching = rules.filter(
20149
- (rule) => applies(rule) && (rule.target === "ALL" || rule.target === target.name) && (rule.cwd === void 0 || rule.cwd === cwd) && stdinMatches(rule, caller.stdin) && commandMatches(rule.cmnd, argv)
20354
+ const matching = mine.filter(
20355
+ (rule) => (rule.target === "ALL" || rule.target === target.name) && (rule.cwd === void 0 || rule.cwd === cwd) && stdinMatches(rule, caller.stdin) && commandMatches(rule.cmnd, argv)
20150
20356
  );
20151
20357
  const grant = () => this.grant(self, target, cred, parsed2.command, path, cwd, call.term);
20152
20358
  if (root || matching.some((rule) => rule.nopasswd)) return grant();
@@ -20157,23 +20363,67 @@ var Sudo = class {
20157
20363
  `sudo: ${self.name} is not allowed to run ${quoted} as ${target.name}`
20158
20364
  );
20159
20365
  }
20366
+ if (!parsed2.reset && this.timestamps.current(s.owner, s.policy)) {
20367
+ this.timestamps.stamp(s.owner, s.policy);
20368
+ this.logged(s, cwd, argv);
20369
+ return grant();
20370
+ }
20160
20371
  if (parsed2.nonInteractive) return failed2("EPERM", "sudo: a password is required");
20161
- if (this.pending.has(cred.ruid)) return failed2("EBUSY", "sudo: another request is pending");
20162
- this.pending.add(cred.ruid);
20163
- try {
20164
- const answer2 = await this.ask(caller, self, target, parsed2.command, path, cwd, signal);
20165
- if (typeof answer2 !== "string") return answer2;
20372
+ return this.pendingFor(cred.ruid, async () => {
20373
+ const answer2 = await this.ask(s, { argv: parsed2.command, path, cwd });
20374
+ if ("error" in answer2) return answer2;
20166
20375
  if (await this.canonical(call.cwd) !== cwd || !await this.reachable(cred, cwd)) {
20167
20376
  return failed2("EACCES", `sudo: ${call.cwd} changed while the request waited`);
20168
20377
  }
20169
20378
  const changed = await this.untrusted(path, cwd, word);
20170
20379
  if (changed) return changed;
20171
- if (answer2 === "once") return grant();
20172
- return await this.always(self, target, cwd, argv, recordable(cwd, parsed2.command), grant);
20380
+ const result = answer2.answer === "once" ? grant() : await this.always(self, target, cwd, argv, recordable(cwd, parsed2.command), grant);
20381
+ if (!("error" in result)) this.stamped(s, answer2);
20382
+ return result;
20383
+ });
20384
+ }
20385
+ async validate(s, root, mine, dir) {
20386
+ const { self, parsed: parsed2 } = s;
20387
+ if (root) return DONE;
20388
+ if (mine.length === 0) {
20389
+ return failed2("EPERM", `Sorry, user ${self.name} may not run sudo on ${this.o.hostname}.`);
20390
+ }
20391
+ if (mine.every((rule) => rule.nopasswd)) return DONE;
20392
+ if (!parsed2.reset && this.timestamps.current(s.owner, s.policy)) {
20393
+ this.timestamps.stamp(s.owner, s.policy);
20394
+ return DONE;
20395
+ }
20396
+ if (parsed2.nonInteractive) return failed2("EPERM", "sudo: a password is required");
20397
+ const cwd = await this.canonical(dir) ?? normalizePath(dir);
20398
+ return this.pendingFor(s.caller.cred.ruid, async () => {
20399
+ const answer2 = await this.ask(s, { argv: [], path: "", cwd, validate: true });
20400
+ if ("error" in answer2) return answer2;
20401
+ if (!answer2.person) await s.caller.report?.(NOT_STAMPED);
20402
+ this.stamped(s, answer2);
20403
+ return DONE;
20404
+ });
20405
+ }
20406
+ async pendingFor(uid, work) {
20407
+ if (this.pending.has(uid)) return failed2("EBUSY", "sudo: another request is pending");
20408
+ this.pending.add(uid);
20409
+ try {
20410
+ return await work();
20173
20411
  } finally {
20174
- this.pending.delete(cred.ruid);
20412
+ this.pending.delete(uid);
20175
20413
  }
20176
20414
  }
20415
+ stamped(s, answer2) {
20416
+ if (answer2.person && !s.parsed.reset) this.timestamps.stamp(s.owner, s.policy);
20417
+ }
20418
+ logged(s, cwd, argv) {
20419
+ const { caller, self, target, owner, policy } = s;
20420
+ const type = policy.type === "tty" && !owner.terminal ? "ppid" : policy.type;
20421
+ const tty = owner.terminal?.tty ?? caller.tty ?? "unknown";
20422
+ const command = argv.map(quoteWord).join(" ");
20423
+ (this.o.warn ?? console.warn)(
20424
+ `sudo: ${self.name} : TTY=${tty} ; PWD=${quoteWord(cwd)} ; USER=${target.name} ; TIMESTAMP=${type} ; COMMAND=${command}`
20425
+ );
20426
+ }
20177
20427
  async always(self, target, cwd, argv, shown2, grant) {
20178
20428
  if (!shown2 || !recordable(cwd, argv)) return { ...grant(), notice: RAN_ONCE };
20179
20429
  if (!await this.remember(self, target, cwd, argv)) {
@@ -20181,7 +20431,7 @@ var Sudo = class {
20181
20431
  }
20182
20432
  return grant();
20183
20433
  }
20184
- listed(self, root, rules) {
20434
+ listed(self, root, rules, defaults2) {
20185
20435
  const host = this.o.hostname;
20186
20436
  if (root) {
20187
20437
  return {
@@ -20194,11 +20444,20 @@ var Sudo = class {
20194
20444
  return failed2("EPERM", `User ${self.name} is not allowed to run sudo on ${host}.`);
20195
20445
  }
20196
20446
  const lines2 = rules.map(described).join("\n");
20197
- return { list: `User ${self.name} may run the following commands on ${host}:
20447
+ const params = defaults2.flatMap((d) => d.params).join(", ");
20448
+ const matching = params === "" ? "" : `Matching Defaults entries for ${self.name} on ${host}:
20449
+ ${params}
20450
+
20451
+ `;
20452
+ return {
20453
+ list: `${matching}User ${self.name} may run the following commands on ${host}:
20198
20454
  ${lines2}
20199
- ` };
20455
+ `
20456
+ };
20200
20457
  }
20201
- async ask(caller, self, target, argv, path, cwd, requester) {
20458
+ async ask(s, asked) {
20459
+ const { caller, self, target, signal: requester } = s;
20460
+ const { argv, path, cwd } = asked;
20202
20461
  const { approve } = this.o;
20203
20462
  if (!approve || requester.aborted) return failed2("EACCES", DENIED2);
20204
20463
  const createdAt = Date.now();
@@ -20217,9 +20476,11 @@ ${lines2}
20217
20476
  ...caller.tty ? { tty: caller.tty } : {},
20218
20477
  createdAt,
20219
20478
  expiresAt: createdAt + timeout,
20220
- always: recordable(cwd, [path, ...argv]),
20479
+ always: asked.validate ? false : recordable(cwd, [path, ...argv]),
20221
20480
  stdin: caller.stdin,
20222
- rootOnly: await this.rootOnly(cwd, argv.slice(1))
20481
+ rootOnly: asked.validate ? {} : await this.rootOnly(cwd, argv.slice(1)),
20482
+ ...asked.validate ? { validate: true } : {},
20483
+ timestamp: true
20223
20484
  };
20224
20485
  const control = new AbortController();
20225
20486
  const timer = setTimeout(() => control.abort("expired"), timeout);
@@ -20229,9 +20490,8 @@ ${lines2}
20229
20490
  try {
20230
20491
  const answer2 = await answerOf(approve, req, control.signal);
20231
20492
  if (answer2 === ABORTED) return ended();
20232
- if (answer2 === "once" || answer2 === "always") {
20233
- return control.signal.aborted ? ended() : answer2;
20234
- }
20493
+ const approved = approvalOf(answer2);
20494
+ if (approved) return control.signal.aborted ? ended() : approved;
20235
20495
  return failed2("EACCES", denialText(answer2));
20236
20496
  } finally {
20237
20497
  clearTimeout(timer);
@@ -23948,7 +24208,7 @@ function replaced(text, values) {
23948
24208
  for (const [ph, value2] of Object.entries(values)) out = out.split(ph).join(binary(value2));
23949
24209
  return out;
23950
24210
  }
23951
- function usable(values) {
24211
+ function usable2(values) {
23952
24212
  const out = {};
23953
24213
  for (const [ph, value2] of Object.entries(values ?? {}))
23954
24214
  if (encoder8.encode(value2).length >= MIN_VALUE) out[ph] = value2;
@@ -23963,7 +24223,7 @@ async function substituteHead(head, ask) {
23963
24223
  if (!request3 || !found || found.length !== 1) return void 0;
23964
24224
  const at = found[0];
23965
24225
  if (at.placeholders.length > SESSION_PLACEHOLDERS) return void 0;
23966
- const values = usable(
24226
+ const values = usable2(
23967
24227
  await ask(`header:${at.name}`, at.placeholders, {
23968
24228
  method: request3[1],
23969
24229
  path: request3[2]
@@ -24273,7 +24533,7 @@ async function cleartext(message3, asks) {
24273
24533
  const placeholders = placeholdersIn(password);
24274
24534
  if (placeholders.length === 0 || placeholders.length > SESSION_PLACEHOLDERS)
24275
24535
  return { bytes: message3, done: true };
24276
- const values = usable(await asks.cleartext(placeholders));
24536
+ const values = usable2(await asks.cleartext(placeholders));
24277
24537
  if (Object.keys(values).length === 0) return { bytes: message3, done: true };
24278
24538
  return { bytes: frame3(112, `${replaced(password, values)}\0`), values, done: true };
24279
24539
  }
@@ -24338,7 +24598,7 @@ var PgClient = class {
24338
24598
  const x = got.exchange;
24339
24599
  const binding = this.header === END_POINT ? this.server.binding : void 0;
24340
24600
  const serverFinal = `c=${channelOf(this.header, binding)},r=${got.nonce}`;
24341
- const values = usable(
24601
+ const values = usable2(
24342
24602
  await this.asks.scram({
24343
24603
  clientFirstBare: x.clientFirstBare,
24344
24604
  serverFirst: x.serverFirst,
@@ -24387,7 +24647,7 @@ function decoded(b642) {
24387
24647
  async function swapped2(text, position, ask) {
24388
24648
  const placeholders = placeholdersIn(text);
24389
24649
  if (placeholders.length === 0 || placeholders.length > SESSION_PLACEHOLDERS) return void 0;
24390
- const values = usable(await ask(position, placeholders));
24650
+ const values = usable2(await ask(position, placeholders));
24391
24651
  if (Object.keys(values).length === 0) return void 0;
24392
24652
  return { text: replaced(text, values), values };
24393
24653
  }
@@ -33034,6 +33294,7 @@ var SUDO_SOURCE = [
33034
33294
  " await say(1, answer.list);",
33035
33295
  " return 0;",
33036
33296
  " }",
33297
+ " if (answer.done === true) return 0;",
33037
33298
  " if (answer.run !== undefined) {",
33038
33299
  " try {",
33039
33300
  " pid = (await ctx.spawn({ argv: answer.run, pgid: 0 })).pid;",
@@ -33673,6 +33934,7 @@ var Launcher = class {
33673
33934
  );
33674
33935
  }
33675
33936
  this.users = new UserDb(this.fs, this.owners);
33937
+ const { sudoClock } = options;
33676
33938
  this.sudoer = new Sudo({
33677
33939
  fs: this.fs,
33678
33940
  users: this.users,
@@ -33682,7 +33944,8 @@ var Launcher = class {
33682
33944
  pnpmHome: this.pnpmHome,
33683
33945
  backing: (path, cwd) => this.backing(path, cwd),
33684
33946
  approve: options.approve,
33685
- timeoutMs: options.sudoTimeoutMs
33947
+ timeoutMs: options.sudoTimeoutMs,
33948
+ now: sudoClock ? () => sudoClock.now() : void 0
33686
33949
  });
33687
33950
  this.cron = this.cronService(options.cron);
33688
33951
  void this.unlinked.sweep("/");
@@ -35088,11 +35351,26 @@ var Launcher = class {
35088
35351
  pid: this.jobs.shown(pid),
35089
35352
  tags: this.tagsOf(pid),
35090
35353
  tty: this.described.get(pid)?.tty,
35354
+ ...this.timestampOwner(pid),
35091
35355
  stdin,
35092
35356
  report
35093
35357
  };
35094
35358
  return this.sudoer.request(caller, call, signal);
35095
35359
  }
35360
+ timestampOwner(pid) {
35361
+ const sid = this.jobs.sidOf(pid);
35362
+ const tty = this.jobs.controllingTerminal(pid)?.name;
35363
+ const leader = sid === void 0 ? void 0 : this.startedOf(sid);
35364
+ const host = this.jobs.shownParent(pid) ?? this.jobs.shown(pid);
35365
+ const started = this.startedOf(host);
35366
+ return {
35367
+ ...tty && sid !== void 0 && leader !== void 0 ? { terminal: { tty, sid, started: leader } } : {},
35368
+ ...started === void 0 ? {} : { parent: { pid: host, started } }
35369
+ };
35370
+ }
35371
+ startedOf(shown2) {
35372
+ return this.running(shown2) ? this.described.get(shown2)?.started : void 0;
35373
+ }
35096
35374
  visudo(pid, call) {
35097
35375
  return this.sudoer.check(this.credOf(pid), call);
35098
35376
  }