Skip to content

Release KSM CLI v1.5.0 - #1065

Open
stas-schaller wants to merge 49 commits into
masterfrom
release/tool/cli/v1.5.0
Open

Release KSM CLI v1.5.0#1065
stas-schaller wants to merge 49 commits into
masterfrom
release/tool/cli/v1.5.0

Conversation

@stas-schaller

@stas-schaller stas-schaller commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Release branch for v1.5.0: 22 CLI bug fixes (including two security fixes: an authorization gap in ksm sync --type aws and an argument-injection gap in ksm init k8s --apply), a dependency cleanup, a behavior improvement to config-source conflict detection, and 10 installer and pipeline fixes from the cli-binaries repo that are user-facing for this release.

Changes

Bug Fixes

  • SSH keyring silent empty return (KSM-929): When the OS keyring (e.g. gnome-keyring) was running but locked with no interactive session to unlock it (common over SSH), the CLI silently returned no profiles. Now raises an actionable error naming the cause and pointing at --ini-file/KSM_CONFIG as a fallback.
  • Config search path resolution (KSM-1113): Windows looked for %APPDIR%\Keeper (a Linux AppImage variable, not a Windows one) instead of %APPDATA%\Keeper; on Linux, the /etc search entry resolved against the current working directory instead of /etc. Both are now resolved correctly.
  • Clone exits 0 on missing source (KSM-1107): ksm secret add clone exited 0 even when the source UID did not exist, masking the failure from scripts. Now exits non-zero with an error message.
  • Clone crashes on empty complex field (KSM-1118): ksm secret add clone crashed with "list index out of range" when the source record contained any unpopulated complex field (name, address, host, etc.) — fields the server legitimately returns as value: []. Empty-value fields are now skipped before the create payload is assembled. Root cause in the helper library is tracked as KSM-1119.
  • Add file/editor crashes on empty complex field (KSM-1126): ksm secret add file and ksm secret add editor crashed with the same "list index out of range" error when the record script contained an unpopulated complex field with value: []. Empty-value fields are now stripped before the create payload is assembled. Root cause: KSM-1119.
  • Download crashes on missing file URL (KSM-1135): ksm secret download crashed with MissingSchema: Invalid URL 'None' when called immediately after ksm secret upload in automation, because the vault may not have propagated the file download URL yet. Now raises a clear error with a prompt to retry in a few seconds. Root cause in the Python SDK: KSM-1131.
  • Clone drops empty custom fields (KSM-1136): The empty-value field skip introduced for KSM-1118 also removed custom fields with value: [] from ksm secret add clone, so empty custom fields were silently missing from the cloned record. Empty custom fields are now re-attached to the clone payload with their type, label, and flags preserved. Regression within this release only; no shipped version affected.
  • Keyring fatal handler masked errors as TypeError (KSM-1155): Every keyring storage failure path in KeyringUtilityStorage crashed with TypeError: KeeperError.__init__() takes 2 positional arguments but 3 were given instead of surfacing the actual error. Now raises KeeperError with the message and chains the original exception as the cause.
  • Shell banner crash on limited stdout encoding (KSM-1156): ksm shell crashed at startup with UnicodeEncodeError when stdout could not represent the Unicode box-drawing banner (e.g. piped/redirected output on Windows, or a C-locale pipe on Linux). The shell now checks the active stdout encoding and falls back to a plain-text banner.
  • Shell session global options dropped inside the shell (KSM-1157): Commands run inside ksm shell re-resolved configuration from scratch, so global options set when launching (--ini-file, --profile-name, --output, --color/--no-color, --cache/--no-cache, --log-level) were ignored for inner commands. Inner commands now inherit the session's global options; an inner line can still override them for that line only. Requires click >= 8.0, now declared in install_requires.
  • Shell corrupts backslash paths on Windows (KSM-1162): click-repl tokenizes input with shlex in POSIX mode where backslash is an escape character, so C:\dir\file.ini typed inside ksm shell became C:dirfile.ini. The shell now uses a safe tokenizer on Windows that treats backslash as a literal character while still stripping quotes normally.
  • Shell truncates arguments at # on all platforms (KSM-1165): The Windows-safe tokenizer introduced for KSM-1162 left # as a comment character, so anything after # in a shell argument was silently dropped — notation references like UID#field/path failed inside the shell. # is now treated as a literal character.
  • keeper.ini discovery: KSM_INI_DIR conflict and NOTSET sentinel (KSM-1163): Two bugs. (A) When KSM_INI_DIR was set and a keeper.ini also existed in the CWD, the CWD file loaded silently. The CLI now warns on stderr naming both paths; set KSM_INI_DIR_SKIP_CONFLICT_WARNING=TRUE to suppress. (B) find_ksm_path() probed _NOTSET_/…/keeper.ini paths when Windows environment variables were unset on POSIX hosts; entries with unset variables are now skipped entirely.
  • Add file/editor drops empty custom fields (KSM-1161): ksm secret add file (and add editor) silently dropped custom fields with value: []. The KSM-1126 workaround strips them before passing to the helper to avoid a crash; those custom fields are now re-attached to the create payload with their type, label, and flags preserved. Root cause: KSM-1119.
  • AWS sync: --prefix now required; dry-run value redacted (KSM-1168): ksm sync --type aws with --record, --folder, or --folder-recursive now requires --prefix. The prefix confines the sync to an operator-defined namespace and prevents a shared-folder collaborator from targeting arbitrary AWS secrets by controlling their record title. Additionally, --dry-run output no longer includes the live destination secret value across all three cloud backends — it now reports dstExists and dstDiffers only. See Breaking Changes below.
  • Azure/GCP sync: dry-run value redacted; destination names validated (KSM-1169): ksm sync dry-run output no longer includes the live destination value for Azure and GCP backends, matching the AWS behavior introduced in KSM-1168. Azure Key Vault and GCP Secret Manager destination names supplied via --map are now validated against provider naming rules before a write.
  • Sync warns when resolving --record/--folder by mutable name (KSM-1170): ksm sync --record/--folder now prints a warning on stderr when a token resolves by record title or folder name/path rather than by UID. Mutable identifiers are fragile in scheduled syncs; the warning names the resolved UID and suggests using UIDs instead.
  • ksm init k8s uses a YAML serializer (KSM-1171): ksm init k8s now builds the Kubernetes Secret manifest with a YAML serializer instead of string formatting, so --name and --namespace are always properly encoded scalars and cannot inject additional manifest content.
  • Sync cloud-dependency errors on frozen binaries (KSM-1182): ksm sync --type aws|azure|gcp gave pip install advice on a missing cloud dependency even in a frozen binary install, where pip cannot fix anything. The three handlers now detect a frozen install and point at the installer's "Cloud Sync" component instead of pip; the AWS pip command is also single-quoted for zsh compatibility.
  • Windows argv expansion corrupts secret values (KSM-1186): click's built-in Windows argument expansion (default since click 8.0) silently corrupted secret values passed on the command line — %VAR%/$VAR were expanded against the environment and $$ was collapsed to $ before the CLI ever parsed them, so password=a$$b was stored as a$b. The same expansion caused ksm exec to deliver raw, unresolved notation to the child process instead of the resolved secret. The CLI now disables this expansion; macOS and Linux are unaffected.
  • AWS sync mutable default argument (KSM-1191): Four AWS sync methods used a mutable list as a default argument (maps: list = []), which Python evaluates once at function-definition time and shares across every call that omits it. Now uses Optional[list] = None, normalized per call.
  • k8s Secret name argument injection (KSM-1183): ksm init k8s --apply passed --name to kubectl as an unvalidated positional argument, so a value beginning with - was read by kubectl as one of its own flags (CWE-88). --name is now validated against the RFC 1123 subdomain rule Kubernetes applies to Secret names before the one-time token is redeemed, and names that are legal in Kubernetes but ambiguous in YAML 1.1 (y, n, 1e5) are now quoted in the manifest output. See Security Impact below.

Behavior

  • KSM_CONFIG overrides an existing keyring profile: When KSM_CONFIG is set and a keyring profile exists that would otherwise take precedence, the CLI now warns on stderr naming the active config source. The warning is suppressed when the keyring is unavailable or empty, so CI/container environments using KSM_CONFIG exclusively see no output.

Dependency

  • colorama removal (KSM-1114): Replaced the unmaintained colorama package with click.style(), already available via the existing click-help-colors dependency. The Windows-only config-permission warning keeps its coloring via a small click.style()-backed shim.

Maintenance

  • Removed ticket ID references from test names and inline comments.

Deferred

  • AWS profile setup non-EC2 crash (KSM-859): fix was written but touched the shared storage package rather than CLI-only code, so it was reverted from this branch. Root-cause fix tracked as KSM-1115 against the storage package (v1.2.0); KSM-859 will close when CLI bumps its pinned storage dependency.

Installer and pipeline fixes (cli-binaries repo — not in this diff, but user-facing for this release)

  • macOS installer crash on every command (KSM-1018): bundled libssl.3.dylib predated OpenSSL 3.2.0, causing ImportError: Symbol not found: _SSL_get0_group_name. Both x64 and arm64 macOS builds now bundle an up-to-date libssl.
  • Windows post-install launch blocked by endpoint security (KSM-1105): InnoSetup's extracted Setup engine parent process in %TEMP% was unsigned, causing EDR/AV to block the post-install launch of ksm.exe.
  • macOS PKG installer ignored component checkboxes (KSM-1106): Keyring and Cloud Sync components were always installed in GUI mode regardless of what the user selected.
  • Windows installer deployed to Program Files (x86) (KSM-1116): Missing ArchitecturesAllowed and ArchitecturesInstallIn64BitMode directives caused the 64-bit binary to land in C:\Program Files (x86) on 64-bit systems.
  • Windows: side-by-side x86 install left after upgrade (KSM-1158): Upgrading from 1.4.0 left the old x86 installation intact and still on PATH. The installer now detects and removes any pre-existing x86 installation before placing the 64-bit binary.
  • Windows: duplicate PATH entry on every install (KSM-1159): The InnoSetup {app} constant was not expanded before the PATH check, appending a duplicate entry on every install or upgrade.
  • Windows: PATH entries written as REG_SZ instead of REG_EXPAND_SZ (KSM-1164): The PATH rewrite introduced by KSM-1159 silently downgraded REG_EXPAND_SZ entries to REG_SZ, preventing Windows from expanding %SystemRoot% and other variable references in PATH. The registry value type is now preserved.
  • Linux tarball has no platform preflight check (KSM-1160): install.sh installed silently on hosts below the documented libc minima; the first ksm invocation crashed with a cryptic loader error. The script now checks the host libc version before installing and exits with an actionable error. Set KSM_SKIP_PREFLIGHT=1 to bypass.
  • Docker alpine image shipped wrong musl version (KSM-1117): The bundled binary required musl >= 1.2.5 while the alpine image base was Alpine 3.19 (musl 1.2.4), so ksm crashed at load time in its own image. The alpine image now uses Alpine 3.22 as its base, with binary self-tests added to the build.
  • Docker arm64 images shipped amd64 binaries (KSM-1120): The mountable /cli/glibc/ksm and /cli/musl/ksm binaries were always the same amd64 ELF on both platforms, making the documented init-container pattern non-functional on arm64 hosts. The arm64 image legs now ship native arm64 binaries.

Security Impact

KSM-1168 eliminates an authorization gap in ksm sync --type aws: a Keeper user who can add records to a synced folder could control their record title to target arbitrary secrets in the AWS account. The --prefix requirement confines every sync operation to an operator-defined namespace. KSM-1168/1169 also redact the live destination secret value from --dry-run output across all three cloud backends, removing a CI-log exfiltration path.

KSM-1183 closes a CWE-88 argument-injection gap in ksm init k8s --apply: an unvalidated --name beginning with - was read by kubectl as one of its own flags rather than as the secret name. RFC 1123 validation now runs before --name ever reaches kubectl, and before the one-time token is redeemed, so a rejected name no longer burns the token.

Breaking Changes

  • ksm sync --type aws now requires --prefix (KSM-1168): users on --record, --folder, or --folder-recursive since v1.2.0 must add --prefix <value> to existing sync commands (e.g. --prefix keeper/). The prefix must end with a non-alphanumeric character to form a real namespace boundary.

Related Issues

  • KSM-929, KSM-1107, KSM-1113, KSM-1114, KSM-1118, KSM-1126, KSM-1135, KSM-1136, KSM-1155, KSM-1156, KSM-1157
  • KSM-1161, KSM-1162, KSM-1163, KSM-1165
  • KSM-1168 (breaking/security), KSM-1169, KSM-1170, KSM-1171
  • KSM-1182, KSM-1183 (security), KSM-1186, KSM-1191
  • Installer-side: KSM-1018, KSM-1105, KSM-1106, KSM-1116, KSM-1158, KSM-1159, KSM-1164, KSM-1160, KSM-1117, KSM-1120
  • Deferred: KSM-859
  • Follow-up (helper root cause): KSM-1119
  • Follow-up (Python SDK root cause): KSM-1131
  • Follow-up (helper custom field flags): KSM-1154
  • Follow-up (in-prefix ownership guard): KSM-1172
  • REL-5258

Ticket IDs in test docstrings/comments rot when the tracker changes
and distract readers into looking something up the comment should
just explain inline; the substance (why each test exists) is kept,
only the KSM-#### / REL-#### / URL references are removed.
_get_instance_region() constructed IMDSFetcher with botocore's default
timeout/retries, so 'ksm profile setup -t aws' stalled on every one of
the four region-lookup calls against an unreachable 169.254.169.254
before eventually surfacing a raw botocore NoRegionError. IMDSFetcher
now uses a 1s/1-attempt timeout, and _get_client() raises a message
naming the non-EC2 cause and the --fallback option instead. read_config()
also logs AWS errors it previously swallowed silently.
…profiles

KeyringUtilityStorage.__load_config() caught every exception, including
keyring.errors.KeyringLocked, behind a blanket except-and-log-at-debug,
so a locked keyring (e.g. gnome-keyring running but locked in an SSH
session with no display server) was indistinguishable from no profile
ever being configured. Reproduced empirically against the actual
published keeper-secrets-manager-cli==1.3.0 package in a Docker
container with a real dbus-daemon + gnome-keyring-daemon, locking a
collection with a stored secret and confirming the silent None return.

__load_config() now catches KeyringLocked specifically and raises the
new KsmCliKeyringLockedException with an actionable message; existing
KsmCliException handling in profile.py surfaces it without further
changes. Keyring-less installs are unaffected (the guarded keyring
import falls back to a tuple that matches nothing on ImportError).
The IMDS timeout fix and _get_client() error handling belong in the
storage package (keeper-secrets-manager-storage), not the CLI release.
These changes will land in storage v1.2.0 via KSM-977-aws-load-config-raise.
APPDIR is a Linux AppImage variable, not a standard Windows env var.
Replace with APPDATA joined with "Keeper" so %APPDATA%\Keeper\keeper.ini
is correctly included in the config path search, consistent with the
existing PROGRAMDATA\Keeper and PROGRAMFILES\Keeper entries.

KSM-1113
Relative ["etc"] entries in find_ksm_path resolved against CWD, not /etc.
os.path.join produces /etc/keeper.ini only when the first component is
an absolute path. No behavior change on Windows where /etc does not exist.

KSM-1113
KSM-1114 dropped colorama, which left check_config_mode()'s Windows
icacls permission warning uncolored (that function, in the Core SDK,
duck-types its color_mod arg against colorama's Fore/Style shape).
A small click.style()-backed shim gives it the same shape without
reintroducing colorama as a dependency.

KSM-1114
…ng entries

Removes the KSM-859 entry (fix was reverted from this branch via 29c5997;
CLI v1.5.0 ships without it, tracked as KSM-1115 for a future release).
Adds KSM-1113, KSM-1107, and the installer-level KSM-1018/1105/1106 fixes,
which were merged on this branch or the same release but missing from
the changelog.
stas-schaller and others added 14 commits July 27, 2026 13:47
FieldType.__init__ in keeper-secrets-manager-helper crashes with
IndexError when a complex field (name, address, host, etc.) has
value: [] — the server's normal representation of an unpopulated field.

Strip fields with empty value lists from the source record before
passing to RecordV3.create_from_data so the helper never receives
value: [] for a dict-typed field. Empty fields on the source are
absent from the clone payload; the server still creates them as empty
on the cloned record.

A proper fix for the root cause in the helper is tracked in KSM-1119.
helper FieldType.__init__ crashes with IndexError when a dict-typed
field (address, name, host, passkey, etc.) has value: [] -- the
server's normal representation of an unpopulated complex field.

The add-file path called Record.create_from_file with no filter, so
any record script containing an unpopulated complex field crashed with
an opaque "list index out of range" error naming neither the field nor
the cause. add editor shares the same loader and is fixed by the same
change.

Fix: load the file via load_file, strip fields where value is empty
before calling RecordV3.create_from_data, matching the pattern
introduced for add clone in KSM-1118 (commit b0d660e). Also adds
cleanup comments at both workaround sites (add file and add clone)
so they can be removed once the helper ships the "if self.value:"
guard in FieldType.__init__ (KSM-1119).

Pre-existing since 1.4.0; not a 1.5.0 regression.
ksm secret download crashed with MissingSchema: Invalid URL 'None' when
called immediately after ksm secret upload in automation. The vault may
not have propagated the file download URL by the time the SDK returns.
CLI-layer workaround checks file.f.get("url") before calling
get_file_data() and raises a clear error with a retry prompt. Remove
when the CLI bumps Python Core past the KSM-1131 fix.
KSM_CONFIG bypasses the KSM-805 keyring integrity check entirely, since
there is no persistent hash to compare an env-var config against. Now
prints a stderr warning when a keyring profile exists and would
otherwise be shadowed. Silent when keyring is unavailable or empty, so
CI/container environments using KSM_CONFIG exclusively see no output.
…t (KSM-1156) (#1074)

The ksm shell startup banner uses Unicode box-drawing characters echoed
unconditionally, so any stdout that cannot encode them - cp1252 when
output is piped or redirected on Windows, or a C-locale pipe on Linux -
crashed the shell at startup with "UnicodeEncodeError: 'charmap' codec
can't encode characters".

Check the active stdout encoding before printing and use a plain-text
banner when the logo cannot be represented. UTF-8 terminals keep the
full logo.
…he shell (KSM-1157)

click-repl re-invokes the main group callback for every line typed in
ksm shell, parsing the inner line against the group's options. Options
absent from the inner line parse as their defaults, so the callback
rebuilt KeeperCli from scratch and session globals were silently
dropped: `ksm --ini-file custom.ini shell` followed by `secret list`
inside the shell resolved config as if --ini-file was never given
(same for --profile-name, --output, --color, --cache and --log-level).

Inner invocations are recognizable by ctx.parent being the session's
group context (top-level runs have no parent). For those, fall back to
the session's stored option values for every option not explicitly
typed on the inner line, detected via ctx.get_parameter_source. Options
typed on an inner line still override the session values, for that line
only. KeeperCli is still rebuilt per line, so flows like profile init
inside a shell keep picking up config changes.

ctx.get_parameter_source requires click 8.0+, so declare click>=8.0 in
install_requires and requirements.txt instead of an unpinned click.
…r (KSM-1155) (#1073)

* fix(cli): raise KeeperError correctly in keyring storage fatal handler (KSM-1155)

KeyringUtilityStorage.__fatal called KeeperError(message, error), but
KeeperError.__init__ only accepts a message, so every storage failure
path crashed with "TypeError: KeeperError.__init__() takes 2 positional
arguments but 3 were given" instead of reporting the actual error.

Raise KeeperError with the message and chain the original exception as
the cause so the real storage error reaches the caller intact.

* fix(cli): only chain keyring fatal cause when one exists (KSM-1155)

Review follow-up: `raise ... from error` with error=None sets
__suppress_context__, which hides the original ImportError from debug
tracebacks when __fatal fires inside the `except ImportError` block
(no keyring backend). Chain the cause only when one was passed, so the
implicit exception context stays visible; adds a regression test
asserting the ImportError context is preserved and not suppressed.
…#1081)

* fix(cli): preserve backslash paths in ksm shell on Windows (KSM-1162)

click_repl 0.2.0 tokenizes typed input with shlex in POSIX mode, where
backslash is an escape character. Windows paths typed inside ksm shell
were corrupted before click saw them (C:\dir\file.ini -> C:dirfile.ini),
and paths with directory separators could even split into spurious extra
tokens. Forward-slash paths were unaffected.

The fix adds a _windows_safe_shlex() context manager that patches
click_repl's shlex reference on Windows for the duration of the REPL
loop. The replacement tokenizer uses posix=True + escape='' +
whitespace_split=True: backslashes are treated as literal characters
rather than escape characters, while quoted paths (including those with
spaces) still have their quotes stripped normally.

The --ini-file case became reachable in 1.5.0 after KSM-1157 made
inner-line global options apply to each command; positional path
arguments (e.g. add file -f) were affected all along.
…warning + _NOTSET_ sentinel (#1082)

* fix(cli): KSM-1163 keeper.ini discovery — KSM_INI_DIR conflict warning and _NOTSET_ sentinel fix

Defect A: when KSM_INI_DIR is set and a keeper.ini also exists in the
current working directory, profile.py's _find_ini_file() was returning
the CWD file without any indication that the env var was being ignored.
The CWD file continues to load (non-breaking — three releases have
shipped with CWD-first, so a silent flip would break setups that now
rely on it), but a yellow warning is now emitted on stderr naming both
paths, which one loaded, and the --ini-file override. Set
KSM_INI_DIR_SKIP_CONFLICT_WARNING=TRUE to suppress the warning.

Defect B: find_ksm_path() used "_NOTSET_" as a placeholder for unset
environment variables and still joined + probed the resulting path.
On any POSIX host where USERPROFILE/APPDATA/PROGRAMDATA/PROGRAMFILES
are unset, those entries resolved to relative _NOTSET_/... paths,
meaning a literal ./_NOTSET_/ directory in the CWD could be
accidentally discovered and loaded. Entries whose environment variable
is unset are now skipped entirely.

Regression coverage: four new tests in IniDiscoveryTest.
…d (KSM-1165)

shlex.shlex sets commenters='#' by default, so everything from '#' to
end-of-line was silently dropped from arguments typed in ksm shell on
Windows (e.g. p@ss#word -> p@ss). Mirror shlex.split(comments=False)
semantics by clearing lex.commenters when comments is False.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
stas-schaller and others added 13 commits August 3, 2026 22:11
…commenters

fix(cli): clear shlex commenters in _win_split so '#' is not truncated (KSM-1165)
…n_non_windows (KSM-1166)

The test read the live sys.platform value, causing it to fail on real
Windows where _windows_safe_shlex() applies the patch. Decorate with
@patch('sys.platform', 'linux') to make the test host-independent.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…platform-mock

test(cli): mock sys.platform in test_windows_safe_shlex_not_applied_on_non_windows (KSM-1166)
…6 workaround in add file (KSM-1161)

The KSM-1126 workaround in add_record_from_file() stripped value:[]
custom fields to prevent a helper IndexError, but unlike the clone
path (KSM-1136), it never re-attached them. Result: ksm secret add
file and ksm secret add editor silently dropped every custom field
whose value was empty.

Save the stripped custom fields before filtering, then zip them back
onto each record_create_obj.custom after create_from_data — the same
pattern the clone path already uses.
…d-file-preserve-custom

fix(cli): re-attach empty-value custom fields dropped by add file workaround (KSM-1161)
… (KSM-1169)

The Azure (sync_azure) and GCP (sync_gcp) dry-run paths placed the current
destination value into the output, and neither backend validated the
destination name before writing. This mirrors the AWS issues in KSM-1168 on
the other two backends; today they are reachable only through operator-supplied
--map keys, but the dry-run value still lands in the sync log.

- Add a shared _dst_status() helper and use it in both dry-run paths so the
  output reports dstExists/dstDiffers instead of the live value.
- Validate Azure and GCP destination names (Azure Key Vault and GCP Secret
  Manager naming rules) before a write; an invalid name is reported per entry
  and skipped rather than sent to the backend.
…ented rules

Address review feedback on KSM-1169. Azure Key Vault object names (secrets)
have no first-character or hyphen-placement restriction; those rules apply to
vault names. Sources: Microsoft Learn (object-name: a 1-127 character string
containing only 0-9, a-z, A-Z, and -), the Key Vault REST spec SetSecret
pattern ^[0-9a-zA-Z-]+$, and PSRule Azure.KeyVault.SecretName. A stricter
regex would refuse names the service accepts, such as 123-secret.

- Expand the validator docstring with the documented rule and sources.
- Pin documented-valid edge shapes (123-secret, name-, a--b) and the 127/128
  length boundary for Azure, and the 255/256 boundary plus 123_id for GCP, so
  any future tightening shows up as a deliberate test change.
…zure-gcp-dryrun-and-validate

fix(cli): redact ksm sync Azure/GCP dry-run output and validate destination names (KSM-1169)
…me (KSM-1170)

_resolve_records matched a token by record title and _resolve_folders matched by
folder name or path, in addition to UID, with no signal to the operator. A title
or folder name is mutable and settable by any shared-folder collaborator, so a
scheduled sync pinned to one is fragile.

Emit a stderr warning naming the resolved UID whenever a token resolves by title
or by name/path rather than by UID. Resolution behavior is otherwise unchanged;
ambiguous matches still error.
…test

Address review feedback on KSM-1170. The folder test now resolves both by
name and by UID in the same mock context and asserts exactly one warning,
matching the shape of the records test, so a regression that made
_resolve_folders warn on UID lookups would fail the test. Also asserts the
warning names the resolved folder UID.
…esolve-by-uid-warning

fix(cli): warn when ksm sync resolves --record/--folder by mutable name (KSM-1170)
…8s-validate-names

fix(cli): build ksm init k8s manifest with a YAML serializer (KSM-1171)
@stas-schaller
stas-schaller force-pushed the release/tool/cli/v1.5.0 branch from 40628aa to 21b22ed Compare August 10, 2026 17:48
stas-schaller and others added 6 commits August 10, 2026 13:52
…ry-run output (#1097)

* fix(cli): KSM-1168 require --prefix for ksm sync --type aws record/folder modes; redact dry-run output

Require --prefix/-px when --record, --folder, or --folder-recursive is used
with --type aws. The prefix is prepended to every AWS secret name derived
from a record title, confining the sync to an operator-controlled namespace
and preventing a folder member from targeting arbitrary secrets in the AWS
account by controlling their record title.

Replace dstValue in dry-run output with dstExists and dstDiffers. The live
destination secret value is no longer fetched or printed, eliminating the
CI-log exfiltration path identified in the Bugcrowd report.

Breaking: users on --record/--folder since v1.2.0 must add --prefix to
existing sync commands.

Closes KSM-1168

* fix(cli): KSM-1168 reject path traversal in record titles used with --prefix

Add _sanitize_title_for_prefix to strip leading slashes and reject '..'
segments from record titles before prepending --prefix. Without this,
a title like '../prod/db-password' with '--prefix keeper/' would produce
'keeper/../prod/db-password', potentially escaping the intended namespace.

* fix(cli): KSM-1168 address review gaps — prefix separator, dstValue compat, test coverage

Validate that --prefix ends with a non-alphanumeric character so the prefix
forms a real namespace boundary (e.g. keeper/ or myapp-); a bare alphanumeric
prefix would concatenate directly into the record title with no delimiter.

Keep dstValue key present with null value in dry-run output instead of
removing it, so consumers using entry["dstValue"] are not broken.

Add tests: _sanitize_title_for_prefix (leading-slash strip, dotdot rejection),
record-based dry-run redaction via sync_aws_json_with_client (json_key=None
branch), and prefix prepend on the --folder path.

* fix(cli): KSM-1168 fix dstDiffers always-true on record dry run; add prefix enforcement and traversal guard tests

* chore(ci): trigger test.cli.yml on release/tool/cli/** branches
KSM-1171 merged immediately after KSM-1169 and KSM-1170; the merge
resolved the README conflict correctly but left the trailing >>>>>>>
end-marker in the file.
…tored byte-for-byte (KSM-1186) (#1102)

* fix(cli): disable click Windows argv expansion so secret values are stored byte-for-byte (KSM-1186)

click >= 8.0 expands every command-line argument on Windows before parsing
(BaseCommand.main -> _expand_args: os.path.expanduser, os.path.expandvars,
then glob). ntpath.expandvars expands %VAR%/$VAR and collapses $$ to $, so
'password=a$$b' was stored as 'a$b' and 'login=x%OS%y' as 'xWindows_NTy' -
silent data corruption of stored secrets, plus premature %VAR% expansion
inside 'ksm exec' command arguments. Windows-only; pip and frozen surfaces.

Pass windows_expand_args=False (the click 8.0.1+ opt-out) in main()'s group
invocation, bump the click floor accordingly, and add a wiring test
(CliRunner bypasses BaseCommand.main(), which is how this evaded the unit
suite - the test asserts the opt-out is passed).

* fix(cli): align requirements.txt click floor with setup.py (>=8.0.1) (KSM-1186)

Review follow-up: requirements.txt still allowed click 8.0.0, which lacks
the windows_expand_args parameter and raises TypeError when main() passes
it. Both dependency declarations now require 8.0.1+.
)

sync_aws_json_with_client, sync_aws_json, sync_aws_with_client and
sync_aws declared maps: list = [], a mutable default argument shared
and mutated across calls that omit it. Switched all four to
Optional[list] = None with maps = maps or [] where the method
iterates without an earlier guard.

Originally found via Datadog SAST triage (VM-2868); ported here onto
cli-1.5.0's current colorama-free sync.py.

KSM-1191

Co-authored-by: Sergey Aldoukhov <[email protected]>
@stas-schaller
stas-schaller force-pushed the release/tool/cli/v1.5.0 branch from 343dd74 to 1b2f962 Compare August 12, 2026 19:42
* fix(cli): validate RFC 1123 secret name in ksm init k8s (KSM-1183)

--name was passed to kubectl as a positional argument with no validation,
so a value beginning with '-' was consumed by kubectl as one of its own
flags instead of as the secret name (CWE-88). KSM-1171 fixed the manifest
branch by switching to yaml.safe_dump() but left the apply branch reading
the name straight into argv.

Validate --name against the RFC 1123 subdomain rule Kubernetes applies to
Secret names, at the top of get_k8s() so the check covers both the apply
and the manifest branch. A validated name can never begin with '-', which
closes the argv surface.

A '--' end-of-flags separator was considered and rejected: kubectl derives
NAME with ArgsLenAtDash(), which counts only the positionals appearing
before '--', so 'kubectl create secret generic -- my-secret' reports
"exactly one NAME is required, got 0". Flags placed after '--' are also
swallowed as positionals, so there is no argument order in which '--'
both preserves NAME and keeps --from-literal parsed as a flag.

The KSM-1171 injection test moves its newline payload to --namespace,
which is still unvalidated and still relies on the YAML serializer;
--name newlines are now rejected up front and are covered by the new
test_k8s_rejects_invalid_names cases.

* fix(cli): anchor RFC 1123 k8s name regex to end-of-string, not end-of-line

Python re's $ matches before a trailing newline as well as at true
end-of-string, so 'my-secret\n' passed the RFC 1123 check the k8s name
validation is supposed to enforce. Switch to \Z, which has no such
exception. Does not affect the leading-dash injection fix (KSM-1183):
a trailing newline can never produce a flag-shaped name.

* fix(cli): allow dots in k8s Secret names, validate before token redemption

Mateo's review of the KSM-1183 fix found three issues in the RFC 1123
check and one in the manifest serializer it protects:

- The regex implemented the label rule (no dots), but Kubernetes Secret
  names follow the subdomain rule, which permits them (tls.example.com,
  v1.2.3). Legal names were being rejected as a regression.
- --name was validated inside get_k8s(), after Init.__init__() already
  redeemed the one-time token over the network, so a rejected name
  still burned the token. Validation now runs in a click callback at
  argument-parse time, before Init() is constructed, matching the
  validate_non_empty idiom already used elsewhere in this file.
- Names that are legal Kubernetes Secret names but ambiguous in YAML
  1.1 (y, n, 1e5) were emitted unquoted in the manifest branch; PyYAML
  itself round-trips them fine, but kubectl's stricter YAML 1.1 parser
  reads them as bool/number instead of string. They're now wrapped in
  a QuotedStr and forced through a SafeDumper subclass that quotes
  only that wrapper type.

Also hoists subprocess.run mocking into _patched_cli, drops scaffolding
nothing in the k8s command path exercises, and wraps the multi-case
invalid-name test in subTest so one failure doesn't mask the rest.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants