From 22dc42e433fe762dc23cd5dd4fa01dfc1dc86173 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Wed, 7 Oct 2026 01:14:19 +0000 Subject: [PATCH] feat(builtins): $SRANDOM, uuidgen, openssl rand Secret generators backed by the OS CSPRNG (same source as /dev/urandom). openssl implements only rand, capped at 1 MiB (TM-DOS-123); uuidgen emits v4 only (L-RAND-001). Claude-Session: https://claude.ai/code/session_019aFikmptPc91Fj4N2iDXQA --- crates/bashkit/docs/compatibility.md | 3 + crates/bashkit/docs/threat-model.md | 1 + crates/bashkit/src/builtins/mod.rs | 2 + crates/bashkit/src/builtins/random.rs | 210 ++++++++++++++++++ crates/bashkit/src/interpreter/mod.rs | 15 ++ .../tests/spec_cases/bash/random.test.sh | 82 +++++++ knowledge/log.md | 1 + knowledge/operations/limitations.md | 1 + knowledge/security/threat-model.md | 1 + knowledge/status/builtins.json | 10 +- 10 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 crates/bashkit/src/builtins/random.rs create mode 100644 crates/bashkit/tests/spec_cases/bash/random.test.sh diff --git a/crates/bashkit/docs/compatibility.md b/crates/bashkit/docs/compatibility.md index fa01c2225..33df5221c 100644 --- a/crates/bashkit/docs/compatibility.md +++ b/crates/bashkit/docs/compatibility.md @@ -97,6 +97,8 @@ for sandbox security reasons. See the compliance spec for details. | `ps` | `-f`, `aux`, `-o`, `-p` | Virtual process list (shell + jobs) | | `pgrep`/`pkill` | `-f`, `-l`, `-a`, `-c`, `-x`, `-SIGNAL` | Match jobs by command | | `disown`, `fg`, `bg` | `[JOBSPEC]` | `fg` waits; `bg` is a no-op (jobs already run) | +| `uuidgen` | `-r` | Random (v4) UUID from the OS CSPRNG | +| `openssl` | `rand [-hex\|-base64] [-out FILE] NUM` | Only `rand`; other subcommands fail with `Invalid command` | | `curl` | `-s`, `-o`, `-X`, `-d`/`--data`, `--data-raw`, `--data-binary`, `--data-urlencode`, `-G`/`--get`, `-H`, `-I`, `-f`, `-L`, `-w`, `--compressed`, `-u`, `-A`, `-e`, `-v`, `-m` | HTTP client (requires http_client feature) | | `wget` | `-q`, `-O`, `--spider`, `--header`, `-U`, `--post-data`, `-t` | Download files (requires http_client feature) | | `time` | `[-p] [-f FORMAT] [-o FILE] [-a] [-v] [--] PIPELINE` | Reserved-word timing; elapsed/status/Bashkit counters are truthful, host CPU/RSS fields say `unavailable` | @@ -319,6 +321,7 @@ Chrono's validated strftime implementation, plus GNU `%N`, `%3N`, `%6N`, and | `$-` | ✅ | Current option flags (POSIX) | | `$_` | ❌ | Last argument | | `$RANDOM` | ✅ | Random number (0-32767) | +| `$SRANDOM` | ✅ | 32-bit random number from the OS CSPRNG (bash 5.1); assignment ignored | | `$LINENO` | ✅ | Current line number | --- diff --git a/crates/bashkit/docs/threat-model.md b/crates/bashkit/docs/threat-model.md index 0ae5bf27e..dca9446fe 100644 --- a/crates/bashkit/docs/threat-model.md +++ b/crates/bashkit/docs/threat-model.md @@ -114,6 +114,7 @@ through configurable limits. | sleep bypasses timeout (TM-DOS-057) | `sleep N` ignores `ExecutionLimits::timeout` | Host-backed timeout; non-JS wasm blocking sleep is clamped to the execution deadline | **MITIGATED** | | Interactive terminal flooding (TM-DOS-119) | Unbounded type-ahead, undrained output, or a `vi` buffer grown by pastes | Caps on unread input (1 MiB), retained output (4 MiB), scrollback (1000 rows), transcript (1 MiB), line length, `vi` buffer (8 MiB) and undo history; `:w` goes through VFS limits | MITIGATED | | Input wait vs. timeout (TM-DOS-120) | Abusing the keyboard-wait exclusion to run work past the deadline | Only time blocked reading the terminal is excluded; CPU work and `sleep` still count | MITIGATED | +| Random generator amplification (TM-DOS-123) | `openssl rand` asked for gigabytes | Requests capped at 1 MiB; fixed-size `uuidgen`/`$SRANDOM` | MITIGATED | | Unbounded builtin output (TM-DOS-058) | `seq 1 1000000` produces 1M lines | Add `max_stdout_bytes` limit | **MITIGATED** | | Silent truncation at builtin caps (TM-DOS-109) | `seq 200000`, an awk loop past its cap, or an oversized `sprintf` expression returns incomplete output with exit 0 | Caps report `: limit () exceeded` on stderr and exit non-zero; awk caps and formatting errors are fatal | **MITIGATED** | | In-builtin memory growth (TM-DOS-110) | `awk 'BEGIN { s = "x"; while (1) s = s s }'` or `jq -n '"x" \| until(false; . + .)'` allocates until the host aborts | awk checks each string against a 16 MiB cap before allocating it, caps `$N` field indexes, and caps total variable memory at `max_live_intermediate_bytes` (fatal, exit 2). jq meters every live string, array and object against the same limit and fails before growing (exit 5); non-emitting jq loops and pure recursion stop at the timeout; non-tail recursion hits a live-context ceiling (64) before host stack exhaustion | **MITIGATED** | diff --git a/crates/bashkit/src/builtins/mod.rs b/crates/bashkit/src/builtins/mod.rs index 9bea4735a..356eee8a2 100644 --- a/crates/bashkit/src/builtins/mod.rs +++ b/crates/bashkit/src/builtins/mod.rs @@ -93,6 +93,7 @@ mod patch; mod path; mod pipeline; mod printf; +mod random; mod read; mod retry; mod rg; @@ -217,6 +218,7 @@ pub use patch::Patch; pub use path::{Basename, Dirname, Readlink, Realpath}; pub use pipeline::{Tee, Watch, Xargs}; pub use printf::Printf; +pub use random::{Openssl, Uuidgen}; pub use read::Read; pub use retry::Retry; pub use rg::Rg; diff --git a/crates/bashkit/src/builtins/random.rs b/crates/bashkit/src/builtins/random.rs new file mode 100644 index 000000000..66652bff5 --- /dev/null +++ b/crates/bashkit/src/builtins/random.rs @@ -0,0 +1,210 @@ +//! Secret and identifier generators: `uuidgen`, `openssl rand`. +//! +//! Decisions: +//! - Bytes come from the OS CSPRNG via `getrandom` (same source as +//! `/dev/urandom`), never from the `$RANDOM` LCG: these are used for +//! tokens, keys and salts. +//! - `openssl` implements only the `rand` subcommand; anything else fails +//! with openssl's own "Invalid command" message so scripts can fall back. +//! - THREAT[TM-DOS-123]: `openssl rand N` is capped at +//! `OPENSSL_RAND_MAX_BYTES` so one call cannot allocate unbounded memory. +//! - `uuidgen` emits random (v4) UUIDs; time-based `-t` and name-based +//! `-m`/`-s` are not supported (no host clock/MAC leakage, L-RAND-001). + +use async_trait::async_trait; +use base64::Engine; + +use super::{Builtin, Context, resolve_path}; +use crate::error::Result; +use crate::interpreter::ExecResult; + +/// Largest `openssl rand` request (1 MiB). +pub(crate) const OPENSSL_RAND_MAX_BYTES: usize = 1024 * 1024; + +fn random_bytes(n: usize) -> std::result::Result, String> { + let mut buf = vec![0u8; n]; + getrandom::fill(&mut buf).map_err(|_| "random source unavailable".to_string())?; + Ok(buf) +} + +/// A random version 4 UUID in lowercase hyphenated form. +pub(crate) fn uuid_v4() -> std::result::Result { + let mut b = random_bytes(16)?; + b[6] = (b[6] & 0x0f) | 0x40; + b[8] = (b[8] & 0x3f) | 0x80; + let h: String = b.iter().map(|x| format!("{x:02x}")).collect(); + Ok(format!( + "{}-{}-{}-{}-{}", + &h[0..8], + &h[8..12], + &h[12..16], + &h[16..20], + &h[20..32] + )) +} + +/// The `uuidgen` builtin: `uuidgen [-r|--random]`. +pub struct Uuidgen; + +#[async_trait] +impl Builtin for Uuidgen { + async fn execute(&self, ctx: Context<'_>) -> Result { + for arg in ctx.args { + match arg.as_str() { + "-r" | "--random" => {} + "-h" | "--help" => { + return Ok(ExecResult::ok( + "Usage: uuidgen [-r|--random]\nCreate a new random (version 4) UUID.\n", + )); + } + "-t" | "--time" | "-m" | "--md5" | "-s" | "--sha1" => { + return Ok(ExecResult::err( + format!("uuidgen: {arg}: not supported in bashkit (random UUIDs only)\n"), + 1, + )); + } + other => { + return Ok(ExecResult::err( + format!( + "uuidgen: invalid option -- '{}'\n", + other.trim_start_matches('-') + ), + 1, + )); + } + } + } + match uuid_v4() { + Ok(u) => Ok(ExecResult::ok(format!("{u}\n"))), + Err(e) => Ok(ExecResult::err(format!("uuidgen: {e}\n"), 1)), + } + } +} + +/// The `openssl` builtin: only `openssl rand [-hex|-base64] [-out FILE] NUM`. +pub struct Openssl; + +const RAND_USAGE: &str = "rand: Use -help for summary.\n"; + +#[async_trait] +impl Builtin for Openssl { + async fn execute(&self, ctx: Context<'_>) -> Result { + let Some((cmd, rest)) = ctx.args.split_first() else { + return Ok(ExecResult::err( + "openssl: interactive mode is not supported in bashkit\n", + 1, + )); + }; + if cmd != "rand" { + return Ok(ExecResult::err( + format!("Invalid command '{cmd}'; type \"help\" for a list.\n"), + 1, + )); + } + let mut hex = false; + let mut b64 = false; + let mut out: Option<&str> = None; + let mut num: Option<&str> = None; + let mut i = 0; + while i < rest.len() { + match rest[i].as_str() { + "-hex" => hex = true, + "-base64" => b64 = true, + "-out" => { + i += 1; + match rest.get(i) { + Some(f) => out = Some(f), + None => return Ok(ExecResult::err(RAND_USAGE, 1)), + } + } + "-help" => { + return Ok(ExecResult::ok( + "Usage: rand [options] num\n -out outfile Output file\n -base64 Base64 encode output\n -hex Hex encode output\n", + )); + } + a if num.is_none() && !a.starts_with('-') => num = Some(a), + a => { + return Ok(ExecResult::err( + format!("rand: Unknown option or number: {a}\n{RAND_USAGE}"), + 1, + )); + } + } + i += 1; + } + let Some(num) = num else { + return Ok(ExecResult::err(RAND_USAGE, 1)); + }; + let n: usize = match num.parse() { + Ok(n) => n, + Err(_) => { + return Ok(ExecResult::err( + format!("rand: Can't parse \"{num}\" as a number\n{RAND_USAGE}"), + 1, + )); + } + }; + if n > OPENSSL_RAND_MAX_BYTES { + return Ok(ExecResult::err( + format!("rand: {n} bytes exceeds the {OPENSSL_RAND_MAX_BYTES} byte limit\n"), + 1, + )); + } + let bytes = match random_bytes(n) { + Ok(b) => b, + Err(e) => return Ok(ExecResult::err(format!("rand: {e}\n"), 1)), + }; + let data: Vec = if n == 0 { + Vec::new() + } else if hex { + let mut s: String = bytes.iter().map(|x| format!("{x:02x}")).collect(); + s.push('\n'); + s.into_bytes() + } else if b64 { + let enc = base64::engine::general_purpose::STANDARD.encode(&bytes); + // openssl wraps base64 at 64 columns. + let mut s = String::with_capacity(enc.len() + enc.len() / 64 + 1); + for chunk in enc.as_bytes().chunks(64) { + s.push_str(&String::from_utf8_lossy(chunk)); + s.push('\n'); + } + s.into_bytes() + } else { + bytes + }; + match out { + Some(f) => { + let path = resolve_path(ctx.cwd, f); + if let Err(e) = ctx.fs.write_file(&path, &data).await { + return Ok(ExecResult::err(format!("rand: {f}: {e}\n"), 1)); + } + Ok(ExecResult::ok(String::new())) + } + None => Ok(ExecResult::ok(crate::StreamData::from(data))), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn uuid_v4_shape() { + let u = uuid_v4().unwrap(); + assert_eq!(u.len(), 36); + assert_eq!(&u[14..15], "4"); + assert!(matches!(&u[19..20], "8" | "9" | "a" | "b")); + assert_ne!(u, uuid_v4().unwrap()); + } + + #[tokio::test] + async fn openssl_rand_limits() { + let mut bash = crate::Bash::new(); + let r = bash.exec("openssl rand -hex 2000000").await.unwrap(); + assert_eq!(r.exit_code, 1); + assert!(r.stderr.contains("limit")); + let r = bash.exec("uuidgen -t").await.unwrap(); + assert_eq!(r.exit_code, 1); + } +} diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 77a284ed0..f235fe360 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -1512,6 +1512,8 @@ impl Interpreter { "readlink" => Readlink, "mkdir" => Mkdir, "mktemp" => Mktemp, + "uuidgen" => Uuidgen, + "openssl" => Openssl, "mkfifo" => Mkfifo, "rm" => Rm, "cp" => Cp, @@ -9677,6 +9679,10 @@ impl Interpreter { }; let resolved: &str = resolved_string.as_str(); // RANDOM=N reseeds the PRNG (matches bash behavior) + // SRANDOM ignores assignment (bash 5.1). + if resolved == "SRANDOM" { + return; + } if resolved == "RANDOM" { self.random_state .store(value.parse::().unwrap_or(0), Ordering::Relaxed); @@ -10377,6 +10383,14 @@ impl Interpreter { self.random_state.store(next, Ordering::Relaxed); return ((next >> 16) & 0x7fff).to_string(); } + "SRANDOM" => { + // $SRANDOM - 32 bits from the OS CSPRNG, not the LCG (bash 5.1). + let mut b = [0u8; 4]; + if getrandom::fill(&mut b).is_err() { + return String::new(); + } + return u32::from_le_bytes(b).to_string(); + } "LINENO" => { // $LINENO - current line number from command span return self.current_line.to_string(); @@ -10448,6 +10462,7 @@ impl Interpreter { | "!" | "-" | "RANDOM" + | "SRANDOM" | "LINENO" | "PWD" | "OLDPWD" diff --git a/crates/bashkit/tests/spec_cases/bash/random.test.sh b/crates/bashkit/tests/spec_cases/bash/random.test.sh new file mode 100644 index 000000000..a2337ed21 --- /dev/null +++ b/crates/bashkit/tests/spec_cases/bash/random.test.sh @@ -0,0 +1,82 @@ +### srandom_is_32bit_number +# $SRANDOM (bash 5.1+) is a fresh 32-bit value from the system CSPRNG. +[[ $SRANDOM =~ ^[0-9]+$ ]] && echo num +(( SRANDOM <= 4294967295 )) && echo fits +[ "$SRANDOM" != "$SRANDOM" ] && echo differs +### expect +num +fits +differs +### end + +### srandom_assignment_ignored +SRANDOM=5 +[ "$SRANDOM" != 5 ] && echo ignored +### expect +ignored +### end + +### openssl_rand_hex +openssl rand -hex 16 | grep -cE '^[0-9a-f]{32}$' +### expect +1 +### end + +### openssl_rand_base64_and_raw +openssl rand -base64 12 | grep -cE '^[A-Za-z0-9+/]{16}$' +openssl rand 7 | wc -c +openssl rand -hex 0 | wc -c +### expect +1 +7 +0 +### end + +### openssl_rand_out_file +openssl rand -out /tmp/key.hex -hex 3 +grep -cE '^[0-9a-f]{6}$' /tmp/key.hex +### expect +1 +### end + +### openssl_rand_bad_count +openssl rand -hex abc +echo rc=$? +### expect +rc=1 +### end + +### openssl_unknown_command +openssl nope 2>/dev/null +echo rc=$? +### expect +rc=1 +### end + +### uuidgen_random_v4 +### bash_diff: uuidgen is not installed on every host +uuidgen | grep -cE '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' +uuidgen -r | wc -c +[ "$(uuidgen)" != "$(uuidgen)" ] && echo unique +### expect +1 +37 +unique +### end + +### password_idioms +# Common secret-generation one-liners. +tr -dc 'A-Za-z0-9' /dev/null +echo rc=$? +### expect +rc=1 +### end diff --git a/knowledge/log.md b/knowledge/log.md index 47f69e732..6aa23424a 100644 --- a/knowledge/log.md +++ b/knowledge/log.md @@ -2,6 +2,7 @@ ## 2026-10-07 +* **Feature**: Secret generators. `$SRANDOM` (bash 5.1, 32 bits from `getrandom`), `uuidgen` (v4 only, L-RAND-001) and `openssl rand [-hex|-base64] [-out] N` (only `rand`; capped at 1 MiB, TM-DOS-123). All draw from the OS CSPRNG like `/dev/urandom`, never the `$RANDOM` LCG. * **Decision**: CPython stdlib ships as bytecode only. Non-preloaded imports compiled source on Pulley every call (3-7 s each); unchecked-hash `.pyc` cut that ~10x. Unusable modules (`ctypes`, `ssl`, network clients/servers, `pdb`, `pydoc`, `_pyrepl`) and pure-Python C twins dropped so the crate stays under the 10 MiB cap. Dropping preloaded modules was rejected: ~1-2 ms first-call gain, seconds per import lost. See [CPython WebAssembly Runtime](runtimes/cpython-wasm.md), [Limitations](operations/limitations.md) L-CPY-009. * **Performance**: Profiled CPython start. A warm `python3 -c 'print(1)'` was ~4.5 ms of Pulley interpreting ~500K guest instructions plus ~1.5 ms in ~340 copy-on-write faults; the first call adds ~13 ms of one-time memory-image build (the 40 MB snapshot span copied into a memfd). Snapshot objects are now immortal (no refcount writes, faults -20%) and the per-call environment is installed from C; warm calls went from ~6.2 to ~5.1 ms. Native AOT instead of Pulley measured 2.3 ms per call but needs executable memory and a 41 MB copy on first load; not taken yet. See [CPython WebAssembly Runtime](runtimes/cpython-wasm.md). diff --git a/knowledge/operations/limitations.md b/knowledge/operations/limitations.md index 7cfd061ea..7618687aa 100644 --- a/knowledge/operations/limitations.md +++ b/knowledge/operations/limitations.md @@ -45,6 +45,7 @@ execution model. Evidence is a threat-model ID, a test, or `stance` | L-PROC-001 | `exec` does not replace the process; `exec cmd` runs cmd then stops execution (fd redirects work) | True process replace would break sandbox containment | TM-ESC-005 | | L-PROC-002 | Background jobs run concurrently (`jobs`, `wait -n`, `kill`, `ps`, `pgrep`), but cannot be stopped or resumed: `kill -STOP/-CONT` are ignored, there is no `suspend`/Ctrl-Z, `bg` is a no-op and `fg` just waits; jobs end when `exec()` returns | No terminal process groups in a virtual shell; a job must not outlive the call that owns it (TM-DOS-122) | `l_proc_002_no_job_control` | | L-PROC-003 | No process spawning; external commands run as builtins | Core sandbox model: no fork/exec escape surface | `l_proc_003_no_process_spawning` | +| L-RAND-001 | `uuidgen` makes random (v4) UUIDs only (no `-t`, `-m`, `-s`); `openssl` implements only `rand` | Time/MAC-based UUIDs would leak host identity; a full TLS/crypto toolkit is out of scope | `uuidgen_time_based_unsupported` | | L-FS-001 | Symlinks are followed, but `..` after a linked directory resolves lexically (`/link/..` is the link's parent, the `cd -L` view), and `ln` without `-s` makes a symlink, not a hard link | The interpreter normalizes paths before the VFS sees them; the VFS has no inodes to share | `symlink.test.sh`, `ln_default_symbolic` | | L-ROOTFS-001 | Default rootfs is static and read-only: `/proc` has no `self`, pid dirs, `uptime` or live counters; `/etc/passwd` has no root entry; `/dev/zero` yields 1 MiB per read; `/bin`, `/usr/bin` are stubs that dispatch builtins | Host state must not leak (TM-INF-003, TM-ISO-018); fixed values keep runs deterministic | `rootfs_layout`, `threat_etc_passwd_blocked` | | L-FS-002 | No file permission enforcement in the VFS | Single-tenant virtual FS; permissions would be theater | `l_fs_002_no_permission_enforcement` | diff --git a/knowledge/security/threat-model.md b/knowledge/security/threat-model.md index f37fe0c3d..37a4f270c 100644 --- a/knowledge/security/threat-model.md +++ b/knowledge/security/threat-model.md @@ -290,6 +290,7 @@ runaway scripts without permanently breaking the session. | TM-DOS-120 | Interactive input wait vs. execution timeout | Excluding keyboard waits from `ExecutionLimits::timeout` (so a `vi` session is not killed while a person or agent thinks) must not let busy work escape the deadline | Only time blocked in the terminal device read is excluded (`InputWaitClock`); the timer re-arms with the remaining *active* budget, so CPU work, `sleep`, and pending callbacks still time out. Only a `Terminal` installs the clock; plain `exec()` is unchanged. Regressions: `terminal::tests::waiting_for_input_does_not_count_toward_timeout`, `terminal::tests::busy_work_still_times_out` | **MITIGATED** | | TM-DOS-121 | `find` traversal amplification | `find -L` over a symlink cycle (`ln -s .. d/up`) re-enters the same directories forever; `-exec ... \;` as a predicate or `{} +` batches could also queue unbounded command lines; the old `-name` glob matcher recursed with string clones per `*` (exponential on `*a*a*...b`) | `-L`/`-H` resolve each directory to its canonical path (realpath with a 40-hop ELOOP cap) and refuse to enter one already on the current path (`File system loop detected`, exit 1); every directory listing charges `consume_work`; find's own output stays capped by `FIND_MAX_OUTPUT_BYTES`; `{} +` batches flush every 4096 paths; `fnmatch` uses a single star restore point (O(value x pattern)). Regression tests: `find_follow_detects_loops` spec case, `ls::glob::tests::many_stars_are_linear` | **MITIGATED** | | TM-DOS-122 | Background job flooding | `while :; do sleep 99 & done`, or a fork bomb `f(){ f & f & }; f`, keeps unbounded concurrent forked interpreters alive | `ExecutionLimits::max_background_jobs` (default 64, `hardened()` 16) caps live jobs; the next `&` prints bash's `fork: retry: Resource temporarily unavailable` and returns 1 without forking. Jobs are polled on the caller's task (no `tokio::spawn`), share the session's command/loop budget, timeout and cancel token, and `exec()` waits for or drops every job before returning, so nothing outlives the call. Regressions: `jobs::tests::*`, `jobs.test.sh` spec cases, `background_job_limit` | **MITIGATED** | +| TM-DOS-123 | Random generator amplification | `openssl rand 99999999999` asks one builtin call for gigabytes of CSPRNG output | `openssl rand` caps a request at `OPENSSL_RAND_MAX_BYTES` (1 MiB) and fails above it; `uuidgen` and `$SRANDOM` emit fixed sizes. Bytes come from `getrandom` (OS CSPRNG), never the `$RANDOM` LCG. Regression: `builtins::random::tests::openssl_rand_limits` | **MITIGATED** | | TM-DOS-101 | yq structured-data amplification | YAML aliases can expand exponentially during deserialization; deep YAML/JSON, document floods, jaq generators, and YAML re-serialization can consume stack, CPU, or memory beyond the source size | Reject YAML alias tokens with a bounded lexical pass before deserialization; VFS/stdin and aggregate input budgets; serde_yaml_ng recursion cap 128 plus Bashkit depth 100; 4096-document cap; shared jaq work/deadline/output limits; post-serialization stdout cap; YAML+JSON depth/document/input/output regressions plus `yq_fuzz` and arbitrary-input proptest | **MITIGATED** | | TM-DOS-107 | Static-analysis command validation amplification | A large comment before thousands of commands makes a whole-script ordered-subsequence scan per command | Build one source-character position index, then validate each analyzed command-name character with a logarithmic position lookup instead of rescanning source | **MITIGATED** | **TM-DOS-051** is historical. The custom parser was deleted with the `yaml` diff --git a/knowledge/status/builtins.json b/knowledge/status/builtins.json index 0ddd5c5e7..d3e0514ae 100644 --- a/knowledge/status/builtins.json +++ b/knowledge/status/builtins.json @@ -421,6 +421,10 @@ "feature": null, "name": "od" }, + { + "feature": null, + "name": "openssl" + }, { "feature": null, "name": "parallel" @@ -725,6 +729,10 @@ "feature": null, "name": "unzip" }, + { + "feature": null, + "name": "uuidgen" + }, { "feature": null, "name": "verify" @@ -778,5 +786,5 @@ "name": "zip" } ], - "count": 194 + "count": 196 }