feat(credentials): store CLI secrets in the OS keyring (AST-164107) - #1545
Open
cx-prathmesh-borle wants to merge 3 commits into
Open
feat(credentials): store CLI secrets in the OS keyring (AST-164107)#1545cx-prathmesh-borle wants to merge 3 commits into
cx-prathmesh-borle wants to merge 3 commits into
Conversation
Signed-off-by: Prathmesh Borle <65400885+cx-prathmesh-borle@users.noreply.github.com>
cx-prathmesh-borle
requested review from
a team,
cx-amol-mane,
cx-rakesh-kadu,
cx-sumit-morchhale and
cx-umesh-waghode
August 24, 2026 10:25
… coverage Ensure explicitly empty credential flags take precedence over environment variables. Track explicit flag state via map presence instead of non-empty values. Fix auth validation flag-combination tests and 7 golangci-lint findings. Add credentialstore/configfile tests to restore coverage above 85%. Signed-off-by: Prathmesh Borle <65400885+cx-prathmesh-borle@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Today the CLI writes the API key and the client secret as plain text in the config file. Any person or program that can read the file can read the secrets. This PR stores these two secrets in the operating system keyring instead. The keyring is the secure credential storage that is built into Windows, macOS, and Linux.
This PR also fixes a broken command,
cx utils env. The old command did not show correct values. The new code fixes this.Related issue: AST-164107.
Branch has one commit:
08c78ef7 feat(credentials): store CLI secrets in the OS keyring (AST-164107).The commit changes 43 files. It adds about 2,372 lines and removes about 226 lines.
The problem
The CLI stores two secret values:
cx_apikey)cx_client_secret)Before this PR, the CLI stored both values as plain text in the file
~/.checkmarx/checkmarxcli.yaml. Plain text means anyone who opens the file can read the secret. This is a security risk.Before this PR, the command
cx utils envalso had a bug. The command read the environment variables with the wrong variable names. As a result, it always showed empty values, even when the values were set. The command looked broken to a user.The goal
cx utils envcommand.High-level design
The CLI now has one component that controls all reads and writes of a secret. This component is the resolver (
internal/credentialstore.Resolver). Every part of the CLI that needs a secret must ask the resolver. No part of the CLI reads or writes a secret directly.The resolver can read a secret from four places. The resolver checks these four places in a fixed order:
--apikey. This value exists only for the current command run.CX_APIKEY.The resolver stops at the first place that has a value. This order does not change between commands.
flowchart LR subgraph WRITE["When the user sets or clears a secret"] W1["cx configure, cx auth login, or cx auth logout"] --> W2["The resolver saves or deletes the value"] W2 --> W3{"Is the keyring turned on?"} W3 -->|"Yes"| W4["Save in the OS keyring"] W3 -->|"No"| W5["Save in the config file"] end subgraph READ["When any command needs a secret"] R1["A command asks the resolver for a value"] --> R2{"Was a flag or environment variable given?"} R2 -->|"Yes"| R6["Use that value"] R2 -->|"No"| R3{"Is the keyring turned on?"} R3 -->|"Yes"| R4["Read the OS keyring"] R3 -->|"No, or value not found there"| R5["Read the config file"] R4 --> R6 R5 --> R6 endThe user controls the keyring with one setting: the environment variable
CX_KEYRING_MODE. This setting has three modes.auto(default)requireddisabledIf the user sets
CX_KEYRING_MODEto an unknown value, the CLI does not stop. It prints a warning and usesautomode instead.How old secrets move to the keyring
The first time the user runs any command after this update, the CLI checks the config file for old secrets. If the CLI finds a secret there, and the keyring mode allows it, the CLI copies the secret into the keyring. After the CLI confirms the copy is correct, it removes the secret from the config file. If the copy step fails, the CLI keeps the secret in the config file and tries again on the next run. The secret is never lost.
flowchart TD A["User runs any CLI command"] --> B["The CLI checks the config file for old secrets"] B --> C{"Is the keyring turned off?"} C -->|"Yes"| D["Do nothing. Keep using the config file."] C -->|"No"| E{"Does the config file have a secret?"} E -->|"No"| F["Nothing to move"] E -->|"Yes"| G["Copy the secret into the keyring"] G --> H{"Did the CLI confirm the copy?"} H -->|"Yes"| I["Remove the secret from the config file"] H -->|"No"| J["Keep the secret in the config file. Try again next time."]Where secrets are stored in the keyring
The CLI stores each secret under one keyring service name,
checkmarx-ast-cli. Inside that service, the CLI creates one keyring entry for the API key and one entry for the client secret.The entry name does not contain the config file path in plain text. Instead, the CLI hashes the file path, and uses part of the hash as the entry name. This has two benefits.
Low-level design
This section lists the main code changes. Function names are in code font. The text next to each name explains what the function does, in plain language.
New folder:
internal/configfileThis folder is new. It is the only code that reads and writes the plain text config file.
Loadreads the file. If the file is missing, it returns an empty result, not an error.Savewrites the file.SetKeywrites one value into the file.ScrubKeyremoves one value from the file.All four functions lock the file before they change it. The lock stops two CLI commands from writing to the file at the same time.
New folder:
internal/credentialstoreThis folder is new. It holds the resolver, the keyring connection, and the migration code.
Resolver.Resolvereads a secret. It follows the four-step order described above in "High-level design".Resolver.Storewrites a secret. It follows the keyring mode setting.Resolver.Cleardeletes a secret. It also follows the keyring mode setting.Migrateruns the one-time copy of old secrets, described above.PolicyFromEnvreads theCX_KEYRING_MODEenvironment variable and turns it into one of the three modes.Changed files that now use the resolver
These files used to read the secret value directly from the configuration library, Viper. Now they call
Resolver.Resolveinstead.internal/wrappers/client.go— sends the API key and the client secret to the Checkmarx server.internal/commands/auth.go— checks if the user is logged in.internal/commands/chat-sast.go— checks if an API key is available for the AI chat feature.internal/commands/agenthooks/mcp/bridge.go— the bridge for the MCP agent integration.internal/wrappers/configuration/configuration.go— thecx configurecommand.internal/commands/util/env.go— thecx utils envcommand.These files used to write the secret value with
viper.Setand a file-write helper function. Now they callResolver.StoreorResolver.Clearinstead.cx configure(both the interactive form andcx configure set)cx auth logincx auth logoutRemoved code
The CLI no longer connects the API key and the client secret to command-line flags through Viper.
internal/params/binds.goandinternal/commands/root.gohad these two connections. This PR removes them, because the resolver now owns both values.The CLI also removes
params.AstToken. This constant stored the short-lived access token inside Viper. The CLI now marks this token as sensitive withlogger.RegisterSensitiveValueinstead. This stops the token from leaking into a config file write, by accident, in the future.Bug fixes in plain language
Fix 1: secrets could appear in debug logs
Before this fix, a user could run a command with
--debug, and the API key or the client secret could show up in the log output. This happened because the log-cleaning code looked for the secret value inside Viper, but Viper no longer held the value after other changes in this PR.The fix: the resolver now tells the logger about every secret value it returns, through
logger.RegisterSensitiveValue. The logger replaces the value with***before it prints anything.Fix 2:
cx configure showdisplayed a blank credentialBefore this fix, the
cx configure showcommand tried to read the secret directly from Viper. Because Viper no longer holds the secret, the command showed a blank value instead of a masked value.The fix: the command now asks the resolver for the value, then masks it, before it shows the result to the user.
Fix 3:
cx utils envshowed wrong informationBefore this fix,
cx utils envhad three problems:The fix: the command now uses a fixed list and a fixed order. It looks up the correct variable name. It shows secrets with only the last four characters visible, for example
******1234.Note for other tools that read CLI output:
cx utils envoutput is different now. Tools that parse this output must expect the new format.Fix 4: a failed
configure seton a secret did not report an errorBefore this fix, if the CLI could not write a secret, for example because the keyring was locked, the
cx configure setcommand still returned a success result.The fix: the command now returns an error, and stops, if the write to the resolver fails.
What changes for the user
cx auth login --helpnow says the login token is stored in the OS keyring, not in the config file.cx utils envoutput looks different. See Fix 3 above.CX_KEYRING_MODE, controls the storage location. The default behavior needs no change from the user.CX_KEYRING_MODE=disabled. The CLI then uses the plain text config file only, the same as before this PR.Testing
go build ./...andgo vetrun clean on all changed packages.golangci-lintreports zero issues in the two new folders.internal/credentialstorepass.internal/commands,internal/commands/util, andinternal/wrapperspass.cx utils env,cx configure set,cx configure show, the interactivecx configure, secret migration from an old config file, the read order (flag, then environment variable, then keyring, then config file), all three keyring modes,cx auth login,cx auth logout, and a check that no secret appears in debug output or log files.CX_KEYRING_MODE=disabledbefore unit tests run. This stops unit tests from touching a real keyring on a developer machine.Known follow-up work, not part of this PR
internal/configfilefolder has no dedicated test file yet.configfile.Saverewrites the whole file in place. If the CLI stops in the middle of this write, the file can become corrupt. A safer method is to write to a temporary file first, then rename it.auth_logout.go, has an import order that does not match the project's code format rule.DefaultStoreis no longer called from anywhere. It can be removed.debug.logand some design notes, should move to thedocsfolder or be deleted before merge.Type of change