wiz-cli is a macOS-first command-line client for the Wiz GraphQL API. It prints JSON by default. Pass -H or --human for tables and readable labelled output.
A threat is not a separate API object. It is an Issue whose type is THREAT_DETECTION. The threats and threat commands use the same issue query and mutation engine as the issue commands. They add the type filter and request threat-detection details.
Install from Homebrew:
brew install rtrompier/tap/wiz-cliInstall from source:
cargo install --git https://github.com/rtrompier/wiz-cliSet the credential variables in the environment before a network operation. Do not put credentials in command arguments or files.
| Variable | Required | Default |
|---|---|---|
WIZ_CLIENT_ID |
Yes | None |
WIZ_CLIENT_SECRET |
Yes | None |
WIZ_API_URL |
Yes | None — your tenant's GraphQL endpoint, https://api.<region>.app.wiz.io/graphql |
WIZ_AUTH_URL |
No | https://auth.app.wiz.io/oauth/token |
XDG_CACHE_HOME |
No | $HOME/.cache |
Authentication uses the OAuth2 client credentials grant with the wiz-api audience.
Grant only the scopes required by the commands the service account will run.
| Operation | Required scope |
|---|---|
Read issuesV2, issue, issueEvidenceRecords, issueHistoryEvents, and issueSuggestedAssignees |
read:issues and read:threat_issues |
Change OPEN to or from IN_PROGRESS with updateIssue |
write:issue_status |
Ignore an issue by moving it to REJECTED or RESOLVED with updateIssue |
write:issue_ignore |
Add a note with createIssueNote |
write:issue_comments |
Assign issues with updateIssuesAssignee |
write:issue_participants |
The service account needs both read scopes. read:threat_issues grants access to query.issuesV2, query.issueEvidenceRecords, and query.issueHistoryEvents; read:issues alone is not enough.
write:issue_status and write:issue_ignore are deliberately separate. Moving between OPEN and IN_PROGRESS changes workflow state. Moving an issue to REJECTED or RESOLVED accepts or closes a security risk and requires write:issue_ignore. A service account with only write:issue_status cannot ignore an issue.
The CLI caches the OAuth access token in $XDG_CACHE_HOME/wiz-cli/token.json. If XDG_CACHE_HOME is unset, it uses $HOME/.cache/wiz-cli/token.json. The directory has mode 0700. The token file has mode 0600. The CLI also corrects an existing token file with looser permissions.
The CLI reuses a token only when it has more than 60 seconds left. A corrupt or unreadable cache falls back to a fresh token request.
List issues. -n defaults to 20:
wiz-cli issues --severity critical --status open -n 50List all threats and include threat details:
wiz-cli threats --threat-runtime-program-name osascript --allthreats fixes the type to THREAT_DETECTION. It rejects --type.
Show one issue:
wiz-cli issue ISSUE-123Show one threat with threat details:
wiz-cli threat ISSUE-123List minimal evidence records:
wiz-cli evidence ISSUE-123List minimal history events:
wiz-cli history ISSUE-123Both queries are verified against a live tenant. Note that Wiz scopes them through filterBy, not a top-level issueId argument, and the two filter fields are spelled differently: issueEvidenceRecords takes filterBy: { issueId } while issueHistoryEvents takes filterBy: { issue }.
To confirm any schema yourself, wiz-cli graphql accepts introspection queries:
wiz-cli graphql 'query { __type(name: "IssueHistoryEventFilters") { inputFields { name } } }'List-valued filters accept repeated flags, comma-separated values, or both:
wiz-cli issues --severity high,critical --status open --status in_progress --project PROJECT-1Enum values are case-insensitive. The CLI validates them before making a request. Date filters accept 30m, 12h, 7d, and 2w forms and convert them to absolute UTC timestamps:
wiz-cli issues --since 7d --created-before 12h --resolved-since 2wUse --all to ignore -n and fetch every page. The CLI requests at most 500 records per page and emits one combined JSON document.
Each mutation command accepts one ID, comma-separated IDs, or - to read newline-separated IDs from standard input.
Close an issue and add its audit note first:
wiz-cli close ISSUE-123 --reason WONT_FIX --note "Risk accepted by the security review"Move issues back into active work:
wiz-cli status ISSUE-123,ISSUE-456 IN_PROGRESSAdd the same note to several issues:
wiz-cli note ISSUE-123,ISSUE-456 "Investigation started"Assign several issues in one API call:
wiz-cli assign ISSUE-123,ISSUE-456 security-owner@example.comFor multi-ID close, status, and note operations, the CLI continues after an individual failure and exits non-zero after printing every per-ID result. Assignment is one batch mutation because the API accepts an ID list.
Every close operation requires write:issue_ignore. The reason determines the target status.
| Resolution reason | Status | Applies to |
|---|---|---|
FALSE_POSITIVE |
REJECTED |
Non-threat issue |
EXCEPTION |
REJECTED |
Non-threat issue |
WONT_FIX |
REJECTED |
Non-threat issue |
NOT_MALICIOUS_THREAT |
REJECTED |
Threat only |
SECURITY_TEST_THREAT |
REJECTED |
Threat only |
PLANNED_ACTION_THREAT |
REJECTED |
Threat only |
INCONCLUSIVE_THREAT |
REJECTED |
Threat only |
MALICIOUS_THREAT |
RESOLVED |
Threat only |
OBJECT_DELETED |
RESOLVED |
Non-threat issue |
ISSUE_FIXED |
RESOLVED |
Non-threat issue |
CONTROL_CHANGED |
RESOLVED |
Non-threat issue |
CONTROL_DISABLED |
RESOLVED |
Non-threat issue |
CONTROL_DELETED |
RESOLVED |
Non-threat issue |
DETECTION_EXPIRED |
RESOLVED |
Non-threat issue |
SEVERITY_CHANGED |
RESOLVED |
Non-threat issue |
On a real close, the CLI fetches all requested issue types in one query. It rejects a threat-only reason for a non-threat issue and a non-threat reason for a threat. Dry-run skips this network validation.
--rejection-expires-days sets rejectionExpiredAt and only works with a reason that maps to REJECTED:
wiz-cli close ISSUE-123 --reason EXCEPTION --rejection-expires-days 90When --note is present, the CLI creates the note before it changes status. If note creation fails, it leaves that issue open.
Pipe newline-separated IDs into a bulk close:
wiz-cli issues --status open --severity critical --all \
| jq -r '.nodes[].id' \
| wiz-cli close - --reason WONT_FIX --note "Bulk risk review completed"Blank input lines are ignored. An empty standard input is an error.
Every mutating command supports --dry-run. It prints the exact GraphQL query and variables in execution order. JSON output is an array of objects with query and variables. -H prints labelled queries and pretty variables.
Dry-run needs no credentials. It does not read or write the token cache and does not use the network.
wiz-cli close ISSUE-123 --reason WONT_FIX --note "Review note" --dry-run
wiz-cli status ISSUE-123 OPEN --dry-run
wiz-cli note ISSUE-123 "Review note" --dry-run
wiz-cli assign ISSUE-123 owner@example.com --dry-runRun a raw GraphQL query with optional variables:
wiz-cli graphql 'query CurrentIssues($first: Int) { issues: issuesV2(first: $first) { totalCount } }' --vars '{"first":1}'Inspect the service account identity, scopes, audience, issuer, expiry, and remaining lifetime without calling GraphQL:
wiz-cli whoami -Hwhoami fetches or reuses an OAuth token and decodes its JWT payload locally. It never prints the access token. It reports the subject, email, tenant, data center and expiry.
It cannot list your granted scopes. Wiz packs permissions into an opaque encodedScopes bitmask with no public decoding table, so the scope list always comes back empty. Read the granted scopes in the Wiz UI on the service account, or probe a command and read the error Wiz returns.