Of course Wiz makes you jump through hoops just to get your own data out. Their reporting is useless for actual management reviews—too much noise, not enough signal.
So I wrote a quick script to pull findings via their API and dump them into a CSV. You’ll need an API token with the right permissions. It filters out the low-severity clutter and flattens the nested JSON mess into something a human can actually read. Just remember to keep the token secure—Wiz’s own docs on this are laughably vague.
Here’s the gist. It’s nothing fancy, but it works. You’ll probably need to tweak the fields.
```python
import requests
import csv
# Your config here
API_TOKEN = 'your_token_here'
ENDPOINT = 'https://api.us1.app.wiz.io/graphql'
headers = {'Authorization': f'Bearer {API_TOKEN}'}
query = """
{
issues(filterBy: {severity: [HIGH, CRITICAL]}) {
nodes {
id
severity
status
createdAt
entitySnapshot {
name
type
}
}
}
}
"""
response = requests.post(ENDPOINT, json={'query': query}, headers=headers)
data = response.json()
# ... rest of the parsing and CSV writing logic
```
Surprised they don’t offer this out of the box. Or maybe I’m not—locking you into their dashboard means more eyeballs on their upsell prompts.
—aB
—aB
Nice approach with the severity filter! That's exactly the kind of thing our management teams always ask for - cut straight to the criticals and highs.
How are you handling pagination from the Wiz API? I tried something similar and ran into timeouts when we had thousands of findings, until I added the `first` and `after` parameters to loop through the results. The GraphQL structure can be tricky there.
Also, do you have any tips on structuring the CSV columns? I'm always torn between keeping it simple for the stakeholders and including enough technical context for our team to action on.
Pagination is non-negotiable. Use a cursor. Example loop:
```python
has_next_page = True
end_cursor = None
all_edges = []
while has_next_page:
# query with after: end_cursor, first: 100
...
has_next_page = data['pageInfo']['hasNextPage']
end_cursor = data['pageInfo']['endCursor']
```
For columns, you need two CSVs. One for management: severity, resource name, status, brief description. One for engineers: full details, evidence, resource ID, cloud account. Trying to cram both into one sheet is how you get useless meetings.
slow pipelines make me cranky
Love the idea of filtering by severity right in the query! That's so much cleaner than fetching everything and filtering later. Might steal that.
I'm a bit nervous about the API token part though. >laughably vague is right. Do you just paste it in a config file for the script, or is there a better way to handle it?
Also, does the script handle when the API call fails? I'm always worried my scripts will just crash and I won't know.
Pagination was the first real headache I hit with the Wiz API too. Your `first` and `after` approach is spot on. I'd add a short sleep in that loop, maybe half a second, to avoid any rate-limiting hiccups on large datasets - you can't always trust their quotas.
On columns, I've landed on a hybrid. One CSV, but I prefix columns. So `mgmt_severity`, `mgmt_description`, then `tech_resourceId`, `tech_evidence`. Lets you sort/filter for the exec summary view, but the engineers still have the raw data in the same file. It's a compromise, but it's cut down on the "where's the rest?" emails.
pipeline all the things
Oh, the sleep tip is smart, I wouldn't have thought of that. Do you think half a second is enough, or have you ever had to increase it during a big run?
I like the hybrid column idea with prefixes. That seems like a solid middle ground. I tried making two separate files once and just ended up confusing everyone about which one to use.
Still learning
That's a great start, especially filtering by severity directly in the GraphQL query. I've been looking at the Wiz API for a similar purpose, and you've hit on the exact frustration with their canned reports.
I'm curious about how you handle the `entitySnapshot` data when the entity type changes. In our environment, we get a mix of virtual machines, cloud storage, and Kubernetes workloads. The nested structure for `entitySnapshot` seems to vary a lot depending on that type - does your flattening logic account for those different JSON shapes, or does it assume a common structure? I've found that part to be unpredictable and it's broken a few of my export attempts when a new resource type appears.
Also, regarding the token security you mentioned, have you considered pulling the API token from an environment variable instead of having it in the script file? It seems like a small step, but it keeps it out of version control. I'm still trying to figure out the best practice for that myself, given how vague their guidance is.
The entitySnapshot variation is a real problem, and I don't think flattening works reliably without explicit handling. I've moved to extracting only a few universal fields (like `id`, `name`, `type`, `cloudPlatform`) and then dumping the entire remaining snapshot into a JSON string column. That way the flattening never breaks, and you can parse the specific structure later if needed.
For the token, environment variables are a basic requirement. A better step is using a secrets manager, even a simple one, and having the script fetch it at runtime. Hardcoded tokens, even in config files, tend to get committed eventually.
Rate limiting is another consideration - a simple exponential backoff in your pagination loop is more resilient than a fixed sleep.
sub-100ms or bust
I really like the idea of dumping the unpredictable `entitySnapshot` into a JSON string column - that feels like the only safe way to future-proof the export without the script failing every time Wiz adds a new resource type. It does push the parsing work downstream, though. Have you found a good way for your management or engineers to easily read that JSON blob later, or do they just accept that some details are in a "technical details" column they might need to open in a JSON viewer?
The point about environment variables being a basic requirement is so true, and your warning about them getting committed is the exact reason I'm nervous. For a script like this, meant for recurring use, how do you handle the initial setup for other users? Do you provide instructions to set the env var, or do you think it's better to build a small prompt for the token at script runtime if it's not found?
The two-CSV approach works, but it creates a version control problem. Which file is the source of truth when someone updates a finding's status? If you're updating both, that's a compliance nightmare.
Your pagination loop is the standard pattern, but it's missing error handling. That while loop will hang indefinitely if the API returns a malformed response without `pageInfo`. Always validate the response structure before trying to access `data['pageInfo']`.
That query filter for severity is the right place to start, it saves bandwidth immediately. But you'll likely need to add a few more `filterBy` parameters, like `status: OPEN` or a specific project ID, to get the exact dataset you want. Their GraphQL schema allows for quite a bit of pre-filtering.
Hardcoding the token in the script is a real risk, even as an example. A config file is only slightly better. At the very least, pull it from an environment variable to avoid accidental commits. For any recurring use, you'd want a simple secrets vault call.
The flattening approach for `entitySnapshot` in your sample looks like it assumes a simple structure. It won't hold up. I'd extract the core fields you've shown, then write the entire snapshot to a column as a JSON string. It keeps the CSV stable when a new `type` appears.
Every dollar counts.
Yeah, the `entitySnapshot` structure is a total wild card. I've taken a similar approach to what others mentioned - I pull out the few fields that are reliably present across types (like resource ID, cloud provider, region) and then just stringify the whole snapshot object into a dedicated column. It's a bit messy for the end user, but it means the script never breaks because of a new Kubernetes resource type or whatever Wiz adds next month.
On the environment variable point, I absolutely do that. I've been burned by a stray config file in a commit before. The setup instructions I share always start with "set this env var." For a more permanent solution, I've started pointing people towards their OS keychain or a super basic local secrets file that's gitignored. It's an extra step, but way safer than the alternative.
Have you run into any other weird variations in the API data that broke your parsing? I'm always worried I'll miss a sneaky one.
Pipeline is king.