A platform-engineering team ships a multi-cloud estate — Terraform for AWS and Azure, CloudFormation for a legacy account, Kubernetes manifests for the shared cluster, and a dozen container images on a common base — and the security team keeps finding the same things days after they merge: an S3 bucket that turned public in a refactor, a :latest base image carrying a Critical OpenSSL CVE, an IAM policy with Action: "*", a Kubernetes Deployment running privileged with hostNetwork: true, a Slack webhook token committed in a values file. The findings arrive in a weekly Wiz cloud report, by which point the misconfiguration is already running in production, already reachable from the internet, already sitting in the code-to-cloud graph as an attack path. The ask from the CISO is blunt: “stop merging the problems.” This guide wires Wiz Code — Wiz’s application-security-posture-management (ASPM) and code-security module — directly into GitHub Actions using the wiz-cli binary so that on every pull request the pipeline scans the infrastructure-as-code, the Dockerfiles, the freshly built image, the secrets in the working tree, and the SBOM, posts findings inline on the PR diff, and fails the required check when a Wiz policy is violated at Critical or High — so the fix happens before merge instead of after deploy.
This is an Advanced, implementation-first walkthrough, and the hands-on lab is the centerpiece: by the end you will have a reusable workflow that authenticates with a Wiz service account (client ID + secret, exchanged for a short-lived token), runs four scan types with real wiz-cli commands, enforces CI/CD policies whose exit codes are the gate, uploads SARIF so findings render as GitHub annotations, produces a CycloneDX SBOM artifact, and blocks the merge via branch protection. We will also place Wiz Code correctly in the real operating model: the code-to-cloud graph and Wiz Sensor runtime context that make a finding prioritized rather than just present, the baselining and ignore workflow so risk-accepted findings do not block forever, admission-style enforcement that carries the gate past CI into the cluster, and the identity, secrets, and ticketing systems that surround it.
Wiz Code is not a standalone scanner you bolt on and forget — its differentiator is that a finding in your Terraform is joined, in the same graph, to the running resource it becomes and the runtime signal from the Wiz Sensor, so the pipeline can tell you this Critical is an internet-exposed, publicly-reachable attack path versus this Critical is a theoretical CVE in a package that never loads. That context is why we fail on some Criticals and warn on others, and it is the throughline of everything below.
What problem this solves
Cloud misconfigurations and vulnerable images do not announce themselves at deploy time; they announce themselves weeks later, in a breach report or an agentless CSPM scan, long after the pull request that introduced them merged clean. The economic asymmetry is the whole argument for shift-left: a misconfiguration caught on a PR diff costs a developer thirty seconds and one commit; the same misconfiguration caught in production costs an incident bridge, a change-freeze, a rebuild, a redeploy, and — if it was reachable — a forensics engagement. Wiz’s own cloud scanning already finds these problems, but it finds them in the running estate, which means the misconfiguration is already live by the time it appears. The gap this guide closes is the window between “a developer wrote the problem” and “the problem is running”: we move detection to the pull request, where the cost of fixing is at its absolute minimum and the person who wrote it is still in context.
What breaks without a pre-merge gate: the security team runs a perpetual game of catch-up against a stream of merges they cannot see until deploy. They file tickets against code that already shipped; developers context-switch back to changes they made a sprint ago; the same class of finding recurs because nothing stops it at the source. Meanwhile the audit story is weak — “we scan the cloud” is not “we prevent the misconfiguration,” and a SOC 2 or FedRAMP assessor will ask which controls block insecure infrastructure from being provisioned, not which controls report on it after the fact.
Who hits this: any team with meaningful infrastructure-as-code and container velocity — a platform team merging dozens of Terraform PRs a week, an application team rebuilding images on every commit, a multi-cloud shop where the same base image and the same module patterns propagate a single mistake across many accounts. It bites hardest where IaC is the only way infrastructure changes (so the PR is the one chokepoint that governs the entire cloud) and where images are built from a shared base (so one bad base CVE lands in every service at once). The fix is not “scan more in the cloud”; it is “make the pull request the gate, and make the gate honest about what actually matters using the graph.”
To frame the whole field before the deep dive, here is every scan class this guide wires, what it inspects, what it catches, and where in the pipeline it runs:
| Scan class | wiz-cli verb |
Inspects | Catches | Runs at |
|---|---|---|---|---|
| IaC misconfiguration | wizcli iac scan |
Terraform, CloudFormation, K8s YAML, ARM/Bicep, Helm, Dockerfile-as-IaC | Public storage, wildcard IAM, unencrypted volumes, privileged pods, open security groups | Before build, on the source tree |
| Container image (vuln) | wizcli docker scan |
Built image layers, OS + language packages | OS/library CVEs with fix status, malware, exposed secrets in layers | After docker build |
| Dockerfile | wizcli docker scan --dockerfile |
The Dockerfile statically | :latest base pins, ADD from URL, hardcoded secrets, missing USER |
Before build, statically |
| Secrets | wizcli dir scan / built into image scan |
Working-tree files, git history, image layers | Verified cloud keys, tokens, private keys, connection strings | Before build, on the tree |
| SBOM | --sbom flags on image/dir scan |
Resolved dependency graph | The full bill of materials (CycloneDX/SPDX) for later VEX/audit | With the image or dir scan |
Learning objectives
By the end of this article you can:
- Create a Wiz service account scoped to CI/CD scanning, exchange its client ID and secret for a short-lived token via
wizcli auth, and source that credential from OIDC-brokered secrets instead of a long-lived key in GitHub. - Run all four
wiz-cliscan types — IaC, Dockerfile, image, and secrets/dir — against a real repository, and read their JSON, human, and SARIF outputs. - Author Wiz CI/CD policies for misconfiguration, vulnerability, secret, and SBOM/license classes, and make each policy’s exit code the pass/fail gate for its step.
- Wire the workflow so findings post inline on the PR diff via SARIF upload, so the SBOM is archived as an artifact, and so an earlier gate failing does not hide a later scan’s annotations.
- Explain how the code-to-cloud graph and Wiz Sensor runtime context turn a raw finding into a prioritized one, and use that to scope which severities fail versus warn.
- Operate the baselining and ignore workflow (
.wizignore, policy ignore rules, baseline files) so intentional, risk-accepted findings do not block indefinitely, with reasons and expiries. - Promote the check to a required status check with branch protection or org rulesets, and carry the gate past CI into admission-style enforcement so drift outside the pipeline is still caught.
- Right-size the whole thing: CI minutes, scan latency, the credential-hygiene story, and where Wiz Code stops and runtime tools (Wiz Sensor, CrowdStrike Falcon) take over.
Prerequisites & where this fits
You should already be comfortable with GitHub Actions — jobs, steps, permissions, the pull_request trigger, and how a job’s exit status becomes a check. You should know your way around Terraform or another IaC format, docker build, and the basic shape of a container image (base layer, OS packages, application layer). You should understand OIDC at the level of “GitHub can mint a short-lived JWT that a trusted party exchanges for a credential” — this guide uses it to avoid a static Wiz secret in the repo. Familiarity with SARIF (the Static Analysis Results Interchange Format GitHub renders as annotations) and SBOM formats (CycloneDX, SPDX) helps but is not required.
Concretely, you need:
| Requirement | Why | How to satisfy |
|---|---|---|
| Wiz tenant with Wiz Code enabled | The module that provides CI/CD scanning, policies, and the graph | Confirm in Wiz console → your subscription includes Code |
| Permission to create a Service Account | The pipeline authenticates as a non-human identity | Wiz Settings → Service Accounts (Admin or Project Admin role) |
| A GitHub repo with Actions enabled + admin | To add the workflow and set branch protection | Repo Settings → Actions on; admin to set required checks |
| A repo that already builds an image and has IaC | The scans need real inputs | Terraform/CFN/K8s in a dir; a working Dockerfile |
The wizcli binary |
The scan engine on the runner | Installed in-job (no manual step); pin a version for prod |
| A secrets source | To supply the Wiz client ID/secret without hardcoding | OIDC → Vault/GitHub OIDC, or (fallback) encrypted Actions secrets |
| (Optional) Wiz Sensor deployed | Runtime context that prioritizes findings | eBPF sensor on your clusters/hosts; not required for scanning |
Where this fits: Wiz Code is the pre-merge, code-security layer of a defense-in-depth posture. Upstream of it sits the broader ASPM story and the cloud-posture (CSPM) scanning covered in Roll Out Wiz CSPM Across a Multi-Account AWS Organization with the AWS Connector — that connector scans the running estate; this guide scans the code that becomes it, and the two share one graph and one console. It pairs tightly with Shift-Left Testing and Quality Gates in CI/CD (Wiz Code is a security quality gate in exactly that model), with Secretless CI/CD: Workload Identity Federation for GitHub Actions and AKS for the OIDC credential story, and with Eliminating Secret Sprawl: Pipeline Scanning, Push Protection, and Leaked-Credential Remediation for the secrets half. On the artifact side it neighbours Deploy Harbor Registry on Kubernetes with Trivy Scanning, Replication, and Cosign Signing (registry-side scanning) and the SBOM-consumption side in Consuming the Software Supply Chain: SBOM Ingestion, VEX Triage, and Admission Verification. Downstream, once the artifact ships, the runtime baton passes to the Wiz Sensor and to Deploy CrowdStrike Falcon Sensor to Linux Fleets and Kubernetes via Helm DaemonSet.
Core concepts
Six mental models make every later decision obvious.
A finding is only as useful as its context, and the graph is the context. Every scanner on earth can tell you an S3 bucket resource has block_public_acls = false. What makes Wiz Code different is that the same finding is a node in the Wiz Security Graph, joined to the cloud resource that Terraform will become, the network exposure that resource has, the identity it can assume, the data it holds, and — if a Wiz Sensor is watching — whether the workload is actually running and reachable. So the pipeline can distinguish “public bucket with PII, internet-reachable, in the production account” (a genuine Critical attack path) from “public bucket in an isolated sandbox with nothing in it” (noise). This is why the gate policy is not “fail on all Criticals” but “fail on Criticals the graph rates as real.”
The service account is a non-human identity, and wizcli auth exchanges it for a short-lived token. Wiz issues a Client ID and Client Secret for a service account. wizcli auth --id <id> --secret <secret> exchanges those for a short-lived bearer token that the CLI caches for the duration of the job and uses for every scan. The client secret is the sensitive material — it should never sit in a workflow file, and ideally never sit as a static GitHub secret either; the OIDC pattern leases it at run time so nothing long-lived lives in the repo.
A CI/CD policy is the contract; the exit code is the enforcement. The decision of what fails the build lives in Wiz as a CI/CD policy — a named rule that says, for example, “fail on Critical and High IaC misconfigurations.” You pass --policy "<name>" to wizcli, the CLI evaluates the scan against that policy, and it exits non-zero when the policy is violated. That non-zero exit fails the GitHub step, fails the job, turns the required check red, and — with branch protection — blocks the merge. Security owns the policy centrally; developers cannot weaken it by editing YAML, because the threshold lives in Wiz, not the workflow.
Scan the Dockerfile and the built image — they catch different things. The Dockerfile scan is static: it reads the instructions and flags a :latest base pin, an ADD from a remote URL, a hardcoded secret in an ENV, a missing non-root USER, before any build happens. The image scan runs after docker build and inspects the resolved layers — the actual base image the tag pointed to, the packages apt-get/pip/npm pulled in, the OS CVEs that are genuinely present. A Dockerfile can look clean and still resolve to a base carrying a Critical CVE; an image can be built from a Dockerfile with an obvious ADD smell. Run both; skip neither.
Findings are stateful across runs — baselining is how you avoid alert fatigue. A gate that fires on every pre-existing finding on the first day is a gate the team disables by lunchtime. Wiz supports baselining (mark the current set of findings as the accepted starting point, and gate only on new findings a PR introduces) and ignore rules (a .wizignore file or a policy-level ignore, each with a reason and an expiry). This is what makes the gate adoptable on a brownfield repo: you draw a line, block regressions, and burn down the backlog on your own schedule — you do not block the whole team on day one for debt they did not create in this PR.
Pre-merge scanning is the first layer, not the only one. The gate stops the misconfiguration in the PR, but infrastructure drifts outside the pipeline (a console change, a break-glass edit, a resource created by another tool), and images that passed on Tuesday grow new CVEs on Wednesday as vulnerabilities are disclosed. So Wiz Code is paired with agentless cloud scanning (continuous CSPM of the running estate) and admission-style enforcement (a gate at deploy/admission time that re-checks the artifact), and with a runtime sensor for threat detection once the workload is live. Shift-left moves detection earlier; it does not replace the layers that watch what actually runs.
The vocabulary in one table
Before the deep sections, pin down every moving part. The glossary at the end repeats these for lookup; this table is the mental model side by side:
| Term | One-line definition | Where it lives | Why it matters to the gate |
|---|---|---|---|
| Wiz Code | Wiz’s ASPM / code-security module | Wiz tenant | Provides wizcli, policies, the code side of the graph |
wizcli |
The scanner CLI on the runner | Installed in-job | Runs every scan; its exit code is the gate |
| Service account | Non-human Wiz identity (client ID + secret) | Wiz → Service Accounts | How the pipeline authenticates, least-privileged |
| CI/CD policy | Named rule deciding pass/fail by severity/class | Wiz → Policies | The contract; --policy invokes it |
| Security Graph | The joined model of code + cloud + runtime | Wiz backend | Turns a raw finding into a prioritized attack path |
| Wiz Sensor | eBPF runtime agent | Your hosts/clusters | Adds “is it actually running/reachable” context |
| SARIF | Static-analysis result format | Emitted by wizcli |
GitHub renders it as inline PR annotations |
| SBOM | Bill of materials (CycloneDX/SPDX) | Artifact of a scan | Feeds VEX, audit, and later admission checks |
| Baseline | The accepted starting set of findings | Wiz / baseline file | Gate on new findings only; brownfield adoption |
.wizignore |
Per-repo documented exceptions | Repo root | Time-boxed, reasoned suppression of specific findings |
| Attack path | A graph chain from exposure to impact | Wiz Graph | The unit of real prioritization, not raw severity |
| Admission gate | Deploy-time re-check of the artifact | K8s admission / CD stage | Carries the gate past CI into the cluster |
The wizcli command surface
Everything the pipeline does is a wizcli invocation, so learn the command surface before the YAML. The CLI is a single static binary; you install it, authenticate once, and run scans. Here is the top-level verb map:
| Command | Purpose | Key inputs | Primary output |
|---|---|---|---|
wizcli version |
Print the CLI version (pin this in prod) | — | Version string |
wizcli auth |
Exchange client ID/secret for a session token | --id, --secret |
Cached token for the job |
wizcli iac scan |
Scan IaC files for misconfigurations | --path, --policy, --output |
Findings + exit code |
wizcli docker scan |
Scan a built image (or a Dockerfile) | --image / --dockerfile, --policy |
Vulns/secrets + SBOM + exit code |
wizcli dir scan |
Scan a directory (secrets, deps, SBOM) | --path, --policy |
Secrets/vulns + exit code |
wizcli docker tag |
Attach scan verdict to an image tag (digest binding) | --image |
Graph linkage of scan → image |
The common flags matter as much as the verbs, because they control the gate, the format, and where results land:
| Flag | Applies to | What it does | Notes |
|---|---|---|---|
--id / --secret |
auth |
Service-account credential | Source from OIDC-leased env vars |
--policy "<name>" |
all scans | Enforce a named Wiz CI/CD policy; non-zero exit on violation | The gate; repeatable for multiple policies |
--path |
iac, dir |
Directory or file to scan | Point at the IaC dir or repo root |
--image |
docker scan |
Image ref (tag or digest) to scan | Scan the digest you will push |
--dockerfile |
docker scan |
Dockerfile to scan statically | Complements the image scan |
--output <fmt,file,bool> |
all scans | Emit results in a format (sarif, json, human) to a file |
The third arg controls whether the file is created on failure too |
--name "<id>" |
all scans | Tag the scan with a build identity | Traceability in the console |
--tag / --project |
all scans | Attach project/labels for graph scoping | Routes findings to the right Wiz project |
A note on exit-code semantics, because the whole gate hinges on it: wizcli returns 0 when the scan passes the named policy, and non-zero when the policy is violated (or the scan errors). A common footgun is treating any non-zero as “the CLI broke” — distinguish a policy failure (the gate doing its job) from a tooling error (auth failed, network down). The table below is the exit-code contract you design steps around:
| Exit code | Meaning | Your step should… |
|---|---|---|
0 |
Scan ran; policy satisfied | Pass; continue |
| non-zero (policy) | Scan ran; policy violated at gate severity | Fail the job (this is the block) |
| non-zero (auth) | wizcli auth failed (bad secret, clock skew) |
Fail fast with a clear message; do not treat as clean |
| non-zero (network/CLI) | Could not reach Wiz / CLI internal error | Fail closed (default) or retry; never silently pass |
Scanning infrastructure-as-code
IaC scanning is where the gate earns most of its keep, because a single Terraform module error propagates to every environment it is applied in. wizcli iac scan --path <dir> walks the directory, identifies the IaC type by content and extension, evaluates every resource against Wiz’s misconfiguration rules (mapped to CIS, the cloud provider benchmarks, and Wiz’s own controls), and returns findings joined to the graph.
Formats it understands
Wiz IaC scanning is multi-format — it is not Terraform-only, which matters in a mixed estate. Point --path at a directory and it discovers what is there:
| IaC format | Detected by | Typical findings surfaced | Notes |
|---|---|---|---|
| Terraform (HCL) | *.tf, *.tf.json |
Public S3/Storage, Action:"*" IAM, unencrypted EBS/disks, open SGs/NSGs |
Scans files; -backend=false needs no remote state |
| Terraform plan (JSON) | plan.json |
Same, evaluated against resolved values | Catches computed values HCL alone hides |
| CloudFormation | *.yaml/*.json templates |
Public resources, wildcard policies, unencrypted stores | JSON and YAML templates both |
| Kubernetes | *.yaml manifests |
privileged: true, hostNetwork, no resource limits, :latest images, over-broad RBAC |
Deployments, DaemonSets, RBAC, PSA gaps |
| Helm | Chart.yaml + templates |
Rendered-manifest misconfigs | Templating resolved before evaluation |
| ARM / Bicep | *.json ARM, *.bicep |
Public Azure resources, missing encryption, weak TLS | Azure-native IaC |
| Dockerfile (as IaC) | Dockerfile |
:latest base, ADD from URL, root user, secrets in ENV |
Overlaps the Dockerfile scan; either surfaces it |
The command and what a run looks like
The core invocation scans a directory and enforces a policy:
wizcli iac scan \
--path ./infra \
--name "kloudvin-iac-${GITHUB_SHA}" \
--policy "Block-Critical-High-IaC" \
--output sarif,wiz-iac.sarif,true
The --output sarif,wiz-iac.sarif,true triple means format = SARIF, file = wiz-iac.sarif, create-on-failure = true — the last flag is what guarantees the SARIF is written even when the policy fails, so the findings still upload to the PR. Human-readable output for local runs uses --output human (or no --output, which prints a table). For a resolved evaluation that catches computed values, feed a plan instead of raw HCL:
# Produce a plan JSON so the scan sees resolved values, then scan it
terraform -chdir=infra init -backend=false
terraform -chdir=infra plan -out=tf.plan
terraform -chdir=infra show -json tf.plan > infra/plan.json
wizcli iac scan --path ./infra --policy "Block-Critical-High-IaC"
Scanning the plan rather than the files matters when a value is computed — a bucket name assembled from variables, a policy built with jsonencode, a CIDR passed in as a variable. Raw-HCL scanning sees the template; plan scanning sees what will actually be created. For the highest fidelity, scan the plan; for the fastest feedback, scan the files. The trade-off table:
| Scan target | Fidelity | Speed | Needs | Use when |
|---|---|---|---|---|
Raw HCL (--path at .tf) |
Good for static values | Fastest (no init/plan) | Nothing | Quick PR feedback, most misconfigs |
| Terraform plan JSON | Highest (resolved values) | Slower (init + plan) | Provider creds or -backend=false |
Computed values, module-heavy code |
| Kubernetes manifests | High for K8s | Fast | Nothing | Cluster workload changes |
| Helm (rendered) | High post-render | Medium | Chart values | Chart changes |
Reading and gating IaC findings
Each finding carries a severity, the rule it violated, the file and line, and — via the graph — the resource it would become and that resource’s exposure. The gate policy should key on severity and graph context, which is why the recommended IaC policy fails on Critical and High but not Medium/Low (those become PR comments, not blocks). The severity-to-action mapping we implement:
| Severity | Example IaC finding | Gate action | Rationale |
|---|---|---|---|
| Critical | Public S3 bucket holding data; Action:"*" admin role |
Fail | Direct, high-impact attack path |
| High | Open security group 0.0.0.0/0:22; unencrypted DB |
Fail | Serious exposure; cheap to fix pre-merge |
| Medium | Missing bucket versioning; no access logging | Warn (PR comment) | Real but not a merge-blocker |
| Low | Missing resource tags; suboptimal defaults | Warn / info | Hygiene; do not block on it |
| Informational | Style/best-practice hints | Info only | Never gate |
Scanning container images and Dockerfiles
Container scanning is the second pillar, and it runs in two complementary passes: the Dockerfile (static, pre-build) and the image (resolved, post-build).
The Dockerfile pass
wizcli docker scan --dockerfile ./Dockerfile reads the instructions and flags anti-patterns before any build cost is paid. What it catches:
| Dockerfile finding | Instruction | Why it matters | Fix |
|---|---|---|---|
| Unpinned base image | FROM node:latest |
:latest is non-reproducible and may carry new CVEs |
Pin a digest or specific tag |
Remote ADD |
ADD https://… /app |
Fetches unverified content at build | Use curl with checksum, or vendor it |
| Hardcoded secret | ENV API_KEY=sk-… |
Secret baked into every layer forever | Use build secrets / runtime injection |
| Runs as root | (no USER) |
Container runs privileged by default | Add a non-root USER |
Broad COPY . . |
COPY . /app |
Copies .git, secrets, junk into the image |
.dockerignore; copy narrowly |
apt without cleanup |
no rm -rf /var/lib/apt/lists/* |
Bloated image, larger attack surface | Clean in the same layer |
The image pass and the SBOM
After docker build, wizcli docker scan --image <ref> inspects the resolved layers: the OS packages, the language dependencies, known CVEs with their fix status, embedded secrets, and (optionally) malware. Crucially, it can extract an SBOM in the same pass, which becomes the artifact you archive for VEX triage and admission verification later:
wizcli docker scan \
--image kloudvin/api:${GITHUB_SHA} \
--name "kloudvin-image-${GITHUB_SHA}" \
--policy "Block-Critical-Fixable-Vulns" \
--sbom-format cyclonedx-json \
--sbom-output sbom.cdx.json \
--output sarif,wiz-image.sarif,true
The most important vulnerability-policy decision is fail on Critical with a fix available, warn on unfixable. Blocking a build on a Critical CVE that has no patch teaches developers to route around the gate — there is no action they can take, so the gate becomes an obstacle rather than a control. Scoping the block to fixable Criticals keeps the gate actionable. The vulnerability-gating matrix we implement:
| Vulnerability class | Fix available? | Gate action | Rationale |
|---|---|---|---|
| Critical CVE, exploitable, on load path | Yes | Fail | Patchable, high-impact — block and fix |
| Critical CVE | No fix yet | Warn + track | No action possible; blocking is noise |
| High CVE | Yes | Fail (or warn, per risk appetite) | Serious; usually block if fixable |
| High CVE | No | Warn | Track via VEX; do not block |
| Medium/Low | Any | Warn / info | Burn down over time, not per-PR |
| Malware signature | — | Fail always | Never ship a flagged binary |
SBOM formats: Wiz can emit CycloneDX or SPDX, in JSON. Which you pick depends on what consumes it downstream:
| SBOM format | Strength | Consumed by | Choose when |
|---|---|---|---|
| CycloneDX JSON | Rich vuln/VEX linkage, component graph | Wiz, Dependency-Track, most scanners | Default for security workflows |
| SPDX JSON | License-focused, ISO-standard, broad tooling | License compliance, SBOM registries | License/compliance is the driver |
Binding the scan to the digest
The scan is only trustworthy if it evaluated the exact artifact you ship. Scan by digest, not a mutable tag, and bind the verdict to that digest so the graph and any admission check reference the same immutable image. Build, capture the digest, scan the digest:
docker build -t kloudvin/api:${GITHUB_SHA} .
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' kloudvin/api:${GITHUB_SHA} 2>/dev/null || echo "kloudvin/api:${GITHUB_SHA}")
wizcli docker scan --image "$DIGEST" --policy "Block-Critical-Fixable-Vulns"
Secret and SBOM scanning
Secrets are the highest-severity, lowest-tolerance finding class: a single verified cloud key in the working tree or git history is an immediate Critical, because it is exploitable the moment it lands in a public or over-shared repo. Wiz scans for secrets in two places — the working tree/directory (wizcli dir scan) and inside image layers (part of the image scan) — and it distinguishes verified secrets (patterns confirmed as live, high-confidence) from unverified candidates (patterns that look like secrets but may be examples).
The secrets-policy posture is the strictest of all: fail on any verified secret, warn on unverified candidates (to avoid blocking on EXAMPLE_KEY=xxxxx placeholders). The secret-gating matrix:
| Secret finding | Confidence | Gate action | Notes |
|---|---|---|---|
| Live AWS/Azure/GCP key | Verified | Fail | Rotate immediately; do not just delete the commit |
| Private key / cert | Verified | Fail | Purge history, rotate the key material |
| DB connection string with creds | Verified | Fail | Move to a secrets manager |
| Token-shaped string | Unverified | Warn | May be a placeholder/example |
| High-entropy string | Heuristic | Info | Review; often false positive |
A critical operational point: finding a secret in a PR means the secret is already compromised and must be rotated, not merely removed from the diff. Deleting the commit does not un-leak a key that was pushed to GitHub’s servers (and possibly forks, caches, and the reflog). This dovetails with the dedicated remediation workflow in Eliminating Secret Sprawl: Pipeline Scanning, Push Protection, and Leaked-Credential Remediation — the gate catches it; that playbook is how you respond.
The dir scan also produces an SBOM for non-container code (a Node/Python/Go project without an image), so the SBOM story covers both containerized and source-only repos:
# Secrets + dependency SBOM for a source tree (no image)
wizcli dir scan \
--path . \
--name "kloudvin-src-${GITHUB_SHA}" \
--policy "Block-Verified-Secrets" \
--sbom-format cyclonedx-json \
--sbom-output src-sbom.cdx.json
The code-to-cloud graph and Wiz Sensor context
This is the section that separates Wiz Code from a generic scanner, and it is why the gate is prioritized rather than noisy. A raw scanner produces a flat list: 400 findings, 30 Critical. A team drowning in 30 “Criticals” — most of which are unreachable, in sandboxes, or theoretical — learns to ignore all of them. Wiz’s answer is the code-to-cloud graph: every finding is a node joined to what it becomes and what it touches.
What the graph joins
The graph connects, for a single finding, the code artifact, the cloud resource it provisions, that resource’s network exposure, the identity it can assume, the data it can reach, and the runtime signal from the sensor. Concretely:
| Graph edge | From | To | What it tells the gate |
|---|---|---|---|
| code → resource | Terraform aws_s3_bucket |
The provisioned bucket | Which running resource this PR creates/changes |
| resource → exposure | The bucket | Internet / VPC / private | Is it publicly reachable? |
| resource → identity | An EC2/pod | The role it assumes | What blast radius a compromise has |
| resource → data | The bucket/DB | Sensitivity (PII/secrets) | What is at stake |
| image → workload | The scanned image | Running pods (via sensor) | Is this image actually deployed? |
| finding → attack path | The chain above | Exposure→identity→data | A ranked, real attack path vs a flat CVE |
Why the Wiz Sensor changes the math
The Wiz Sensor is an eBPF agent on your hosts and clusters. It is not required for scanning — you can gate PRs with Wiz Code and no sensor — but when present, it feeds runtime signal into the graph that transforms prioritization. Two examples make the difference concrete:
- A Critical CVE in a package that, per the sensor, is never loaded at runtime in any deployed workload → deprioritized. You do not block the PR on a vulnerability in dead code.
- A Critical CVE in a library that the sensor confirms is loaded in a pod that is internet-exposed and holds a role with
s3:*→ escalated to a genuine, top-of-queue attack path.
This is runtime-informed prioritization, and it is the argument for scoping your gate policy to graph-rated severity rather than raw CVSS. The comparison:
| Prioritization basis | What it uses | Failure mode | Wiz Code position |
|---|---|---|---|
| Raw CVSS severity | The CVE score alone | Alert fatigue; 30 “Criticals,” all noise | Insufficient alone |
| Fix-available filter | CVSS + patch status | Better, still context-free | Necessary, not sufficient |
| Reachability (code) | Is the code path called? | Static reachability is approximate | Good signal |
| Runtime (sensor) | Is it loaded + exposed + privileged? | Requires sensor coverage | The differentiator |
| Attack-path (graph) | All of the above joined | Requires the graph | The Wiz model — gate on this |
Practically, this means: deploy the sensor where you can (it makes findings dramatically more accurate), but design the gate so it works without the sensor (fixable-Critical + graph-exposure is a solid policy even sensor-free), and let the sensor tighten prioritization where it is present. Where Wiz Code stops and the sensor/EDR take over is the boundary of this article — for runtime threat detection on the workloads once they ship, see Deploy CrowdStrike Falcon Sensor to Linux Fleets and Kubernetes via Helm DaemonSet.
Policy gates and CI/CD policies
The gate is a Wiz CI/CD policy invoked by --policy. Getting the policy set right — one per scan class, scoped by severity and graph context — is the difference between a gate the team respects and one they route around. Create these in Wiz console → Policies → Create Policy → CI/CD.
The policy set
The recommended baseline is four policies, one per class, each named so wizcli can reference it:
| Policy name | Class | Fails on | Warns on |
|---|---|---|---|
Block-Critical-High-IaC |
IaC misconfig | Critical + High misconfigurations | Medium/Low |
Block-Critical-Fixable-Vulns |
Image/dep vulns | Critical CVEs with a fix + malware | Unfixable Critical; High |
Block-Verified-Secrets |
Secrets | Any verified secret | Unverified candidates |
Warn-License-SBOM |
SBOM/license | (nothing — report only) | Copyleft/denied licenses |
Where the enforcement lives
The exit code is the mechanism. Each wizcli … --policy <name> step exits non-zero on violation, which fails the job step, which fails the job, which fails the required check. Three properties make it a real gate rather than a report:
- Non-zero exit on policy violation → the step and job fail. This is the block.
- SARIF upload with
if: always()→ findings post inline on the PR even when an earlier gate failed (otherwise the failing scan hides its own annotations). --name/--projecttagging → results land in the Wiz console under a traceable build identity, so security sees the same finding the developer sees.
A subtle point on ordering and set -e: if you run multiple scans in one shell step, an early failure with set -e (the default in run: blocks) aborts before later scans run. For independent gates you usually want each scan as its own step so all of them run and all of them annotate, with the job failing if any failed. The exemplar workflow below does exactly that.
Fail-open vs fail-closed
A design decision with security consequences: if wizcli cannot reach Wiz (network blip, auth expiry), should the step pass (fail-open) or fail (fail-closed)? For a security gate the default is fail-closed — a scan that could not run is not a scan that passed. Fail-open is only defensible as a temporary, logged, time-boxed measure during a known Wiz outage, and even then you are accepting unscanned merges. The trade-off:
| Mode | On scan error | Pro | Con | Use when |
|---|---|---|---|---|
| Fail-closed (default) | Block the merge | No unscanned code merges | A Wiz outage blocks all merges | Almost always |
| Fail-open | Allow the merge | Merges continue during outage | Unscanned code ships | Only a logged, time-boxed break-glass |
PR annotations, comments, and developer experience
A gate that only turns a check red teaches developers nothing about what to fix. The experience that changes behavior is the finding rendered on the exact line of the diff, and Wiz Code delivers that through SARIF upload plus (optionally) PR comments.
SARIF → GitHub annotations. Each scan emits a SARIF file; github/codeql-action/upload-sarif ingests it and GitHub renders each finding as an annotation on the changed line, and in the Security → Code scanning tab. The developer sees “Public S3 bucket — line 42” without leaving the PR. The category: on the upload separates IaC findings from image findings so they do not collide in the Security tab.
Wiz native PR comments. Beyond SARIF, the Wiz GitHub app (if installed) can post a summary comment on the PR — a roll-up of new findings, severities, and links to the graph context in the console. This is the “here is the attack path, not just the CVE” view that SARIF alone cannot convey.
The developer-experience surfaces, compared:
| Surface | Source | Granularity | Shows graph context? | Best for |
|---|---|---|---|---|
| Inline annotation | SARIF upload | Per line | No (just the finding) | “Fix this exact line” |
| Security → Code scanning tab | SARIF upload | Per finding, historical | No | Triage over time |
| PR summary comment | Wiz GitHub app | Roll-up per PR | Yes (links to attack path) | “Is this a real risk?” |
| Wiz console | The scan --name |
Full graph | Yes | Security-team deep dive |
| Check status (red/green) | Job exit code | Pass/fail | No | The merge gate itself |
Baselining, ignores, and exception hygiene
The single biggest reason security gates get disabled is first-day alert fatigue on a brownfield repo. Turning on a Critical-blocking gate against a codebase with 200 pre-existing findings blocks every PR immediately, for debt no individual PR created. The fix is disciplined suppression, and Wiz gives you three tools with different scopes.
The three suppression mechanisms
| Mechanism | Scope | Where defined | Lifetime | Use for |
|---|---|---|---|---|
| Baseline | The whole repo’s current findings | Wiz project / baseline file | Until re-baselined | Brownfield adoption: gate on new only |
.wizignore |
Specific finding IDs/paths | Repo root file | Per-entry expires |
Documented, time-boxed exceptions in code |
| Policy ignore rule | A finding class org-wide | Wiz console policy | Until removed | Systematic false positives / accepted patterns |
Baselining draws a line: mark today’s findings as accepted, and the gate fails only on findings a PR introduces. This is what makes adoption humane — you block regressions immediately, then burn down the baseline on a schedule, without holding the whole team hostage to pre-existing debt. Re-baseline deliberately (not automatically), so you do not silently accept new debt.
.wizignore lives in the repo, is reviewed like code, and every entry carries a reason and an expiry. It is for the specific, justified exception — “this demo bucket is intentionally public, non-prod, expires 2026-09-01.” The format:
cat > .wizignore <<'EOF'
# finding-id / rule path (optional) reason expires
wiz-iac-aws-s3-public infra/sandbox/* demo bucket, non-prod only 2026-09-01
wiz-vuln-CVE-2025-XXXXX - no fix upstream; tracked in JIRA-1234 2026-08-15
EOF
The discipline that keeps .wizignore from becoming a junk drawer — the failure mode that hollows out the gate:
| Rule | Why | Enforce by |
|---|---|---|
| Every entry has a reason | An unexplained ignore is invisible risk | PR review; reject reasonless entries |
| Every entry has an expiry | Permanent ignores never get revisited | CI lint that flags expired/expiring entries |
| Entries are reviewed by security, not devs alone | Devs should not self-approve weakening the gate | CODEOWNERS on .wizignore → security team |
| Prefer narrow paths over broad globs | A broad ignore suppresses future real findings | Review scope; scope to the specific dir |
| Fix beats ignore | An ignore is deferred risk, not resolved risk | Track ignores as debt; burn them down |
The exception lifecycle
An ignore is a promise to revisit, so it needs a lifecycle: created (with reason + expiry) → reviewed (by security) → tracked (as debt) → expired (CI flags it) → resolved-or-renewed (fix it or consciously re-accept). The lifecycle table:
| Stage | Trigger | Owner | Artifact |
|---|---|---|---|
| Create | Justified false-positive / accepted risk | Developer | .wizignore entry (reason + expiry) |
| Review | PR touching .wizignore |
Security (CODEOWNERS) | Approved diff |
| Track | Entry merged | Security | Debt ticket (JIRA/ServiceNow) |
| Expire | Expiry date reached | CI lint | Failing lint / flagged PR |
| Resolve | Fix lands, or risk re-accepted | Developer + Security | Removed entry or renewed expiry |
Admission-style enforcement: carrying the gate past CI
CI is the first gate, not the last. Two gaps make CI-alone insufficient: infrastructure drifts outside the pipeline (a console change, a break-glass edit, a resource created by a different tool never sees the PR gate), and an image that passed on Tuesday grows new CVEs by Thursday as vulnerabilities are disclosed. Admission-style enforcement re-checks the artifact at deploy time, so the same policy that gated the PR also gates what actually reaches the cluster.
The layered enforcement model — each layer covers a gap the previous one leaves:
| Layer | Where it runs | Catches | Gap it closes |
|---|---|---|---|
| PR gate (this guide) | GitHub Actions on pull_request |
Bad IaC/image/secret before merge | The moment of authorship |
| Registry scan | On push to the registry | Vulns discovered after build | New CVEs on a stored image |
| Admission control | K8s admission at deploy | Unscanned/failing images reaching the cluster | Images that bypass CI |
| Agentless CSPM | Continuous cloud scan | Drift introduced outside IaC | Console/break-glass changes |
| Runtime sensor | eBPF on the workload | Exploitation of what shipped | Post-deploy threats |
Admission control (via an admission controller keyed on the Wiz scan verdict, or a policy engine that consults the graph) refuses to admit a pod whose image has not passed policy — so even an image pushed by a path that skipped CI is stopped at the cluster door. This is the same digest-bound verdict the PR scan produced, which is why scanning by digest (not a mutable tag) in CI matters: the admission check references the exact artifact CI evaluated. The deeper mechanics of admission verification against SBOM/VEX are covered in Consuming the Software Supply Chain: SBOM Ingestion, VEX Triage, and Admission Verification.
The CI wiring: OIDC, service accounts, and secret hygiene
The workflow needs the Wiz client ID and secret to authenticate, and how it gets them is a security decision in its own right. The worst option (hardcoded in the YAML) is off the table; the acceptable-but-weaker option is an encrypted Actions secret; the strong option is OIDC-leased at run time so nothing long-lived lives in GitHub. The comparison:
| Credential source | Long-lived secret in GitHub? | Blast radius if repo leaks | Effort | Recommendation |
|---|---|---|---|---|
| Hardcoded in workflow | Yes (in git!) | Total — key is in history | — | Never |
| Encrypted Actions secret | Yes (in GitHub secret store) | The static secret leaks | Low | Acceptable fallback |
| OIDC → Vault (lease at run) | No | Nothing static to leak; TTL-bound | Medium | Preferred |
| OIDC → cloud secrets manager | No | Nothing static; scoped by claims | Medium | Also strong |
The OIDC pattern: GitHub mints a short-lived JWT for the job (requires id-token: write), a broker (HashiCorp Vault, or a cloud secrets manager with OIDC trust) validates the JWT’s claims (repo, ref, environment) and returns the Wiz secret with a short TTL, and wizcli auth uses it. The Wiz secret never sits in GitHub. Store the credential in Vault and configure the trust:
# Store the Wiz CI/CD credential in Vault (KV v2)
vault kv put secret/ci/wiz \
client_id="$WIZ_CLIENT_ID" \
client_secret="$WIZ_CLIENT_SECRET"
# Trust GitHub's OIDC issuer
vault auth enable -path=github-actions jwt
vault write auth/github-actions/config \
oidc_discovery_url="https://token.actions.githubusercontent.com" \
bound_issuer="https://token.actions.githubusercontent.com"
# A role bound to this repo + PR refs, with a short token TTL
vault write auth/github-actions/role/wiz-scan \
role_type="jwt" \
user_claim="actor" \
bound_audiences="https://github.com/kloudvin" \
bound_claims_type="glob" \
bound_claims='{"repository":"kloudvin/*","ref":"refs/pull/*"}' \
token_policies="wiz-ci-read" \
token_ttl=15m
The bound_claims restriction is what stops a fork or an unrelated repo from minting the credential — it ties issuance to your repo and PR refs. This is the same secretless-CI/CD pattern detailed in Secretless CI/CD: Workload Identity Federation for GitHub Actions and AKS and the broader CI/CD Secrets and Credential Management: Secure Your Pipelines; for syncing the secret into other runtimes see Set Up External Secrets Operator to Sync Vault and AWS Secrets into Kubernetes.
The service account itself must be least-privileged. Scope it to CI/CD scanning only — it does not need read over your whole cloud inventory, only the ability to run scans and post their results. The permission posture:
| Service-account scope | Grants | Risk if leaked | Use for |
|---|---|---|---|
| CI/CD scanning (recommended) | Run scans, submit results, evaluate policy | Attacker can run scans (low value) | The pipeline |
| Project-scoped read | Read one project’s findings | One project’s findings exposed | Reporting integrations |
| Tenant admin | Everything | Total tenant compromise | Never for CI |
The workflow YAML, end to end
Here is the complete workflow — the heart of the integration. It leases the credential via OIDC, installs a pinned wizcli, authenticates, and runs four gated scans each as its own step (so all annotate even if one fails), uploads SARIF, and archives the SBOM. Create it as .github/workflows/wiz-code-gates.yml:
name: Wiz Code Security Gates
on:
pull_request:
branches: [main]
permissions:
contents: read
id-token: write # required: GitHub OIDC -> Vault
security-events: write # required: upload SARIF to the PR / Security tab
pull-requests: write # required: PR comments
env:
WIZCLI_VERSION: "latest" # pin a real version in production, e.g. "1.x.y"
jobs:
wiz-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # full history so secret scanning sees prior commits
# Lease the Wiz CI/CD credential from Vault via GitHub OIDC (no static secret)
- name: Import Wiz secret from Vault
id: vault
uses: hashicorp/vault-action@v3
with:
url: https://vault.kloudvin.internal:8200
method: jwt
path: github-actions
role: wiz-scan
secrets: |
secret/data/ci/wiz client_id | WIZ_CLIENT_ID ;
secret/data/ci/wiz client_secret | WIZ_CLIENT_SECRET
- name: Install wizcli
run: |
curl -sSLo wizcli "https://wizcli.app.wiz.io/${WIZCLI_VERSION}/wizcli"
chmod +x wizcli
sudo mv wizcli /usr/local/bin/wizcli
wizcli version
- name: Authenticate wizcli to the Wiz tenant
run: wizcli auth --id "$WIZ_CLIENT_ID" --secret "$WIZ_CLIENT_SECRET"
# GATE 1 — IaC misconfiguration (Terraform / CFN / K8s / ARM)
- name: Wiz IaC scan
run: |
wizcli iac scan \
--path ./infra \
--name "kloudvin-iac-${{ github.sha }}" \
--policy "Block-Critical-High-IaC" \
--output sarif,wiz-iac.sarif,true
- name: Upload IaC SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: wiz-iac.sarif
category: wiz-iac
# GATE 2 — Secrets in the working tree + full git history
- name: Wiz secrets (dir) scan
run: |
wizcli dir scan \
--path . \
--name "kloudvin-secrets-${{ github.sha }}" \
--policy "Block-Verified-Secrets"
# GATE 3 — Dockerfile (static, pre-build)
- name: Wiz Dockerfile scan
run: |
wizcli docker scan \
--dockerfile ./Dockerfile \
--name "kloudvin-dockerfile-${{ github.sha }}" \
--policy "Block-Critical-High-IaC"
# Build the exact artifact we intend to ship
- name: Build image
run: docker build -t kloudvin/api:${{ github.sha }} .
# GATE 4 — Image layers + SBOM (post-build, resolved)
- name: Wiz image scan (+ SBOM)
run: |
wizcli docker scan \
--image kloudvin/api:${{ github.sha }} \
--name "kloudvin-image-${{ github.sha }}" \
--policy "Block-Critical-Fixable-Vulns" \
--sbom-format cyclonedx-json \
--sbom-output sbom.cdx.json \
--output sarif,wiz-image.sarif,true
- name: Upload image SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: wiz-image.sarif
category: wiz-image
- name: Archive SBOM
if: always()
uses: actions/upload-artifact@v4
with:
name: sbom-${{ github.sha }}
path: sbom.cdx.json
Why each design choice is deliberate — the annotated rationale:
| Choice | Why |
|---|---|
pull_request to main only |
Gate what will merge; avoid scanning every branch push (cost) |
| Each scan a separate step | With set -e, one failing scan won’t skip the others — all annotate |
if: always() on uploads |
Findings still post when an earlier gate failed (else they hide) |
Pin WIZCLI_VERSION in prod |
A CLI release can change exit semantics; pin for reproducibility |
fetch-depth: 0 |
Secret scanning needs history, not just the tip commit |
| Scan Dockerfile and image | Static smells + resolved CVEs are different finding sets |
SBOM archived on always() |
You want the bill of materials even on a failing run |
id-token: write present |
Without it the OIDC handshake to Vault 403s and the job dies pre-scan |
Making the check required (branch protection)
A red check that does not block merge is theater. Promote the job to a required status check so GitHub refuses the merge while it is failing. Via the GitHub CLI:
gh api -X PUT repos/kloudvin/api/branches/main/protection \
-H "Accept: application/vnd.github+json" \
-f 'required_status_checks[strict]=true' \
-f 'required_status_checks[contexts][]=wiz-scan' \
-F 'enforce_admins=true' \
-f 'required_pull_request_reviews[required_approving_review_count]=1' \
-F 'restrictions=null'
enforce_admins=true is deliberate — the gate should bind everyone, including the people who can edit it, or it will be bypassed under deadline pressure. strict=true requires the branch to be up to date before merge, so the scan that ran is the scan of what will actually land. If your org standardizes via rulesets, create an org ruleset targeting main with a “Require status checks to pass” rule naming wiz-scan; the effect is identical and applies across every repo at once. The protection knobs that matter for a security gate:
| Setting | Value | Effect | Why |
|---|---|---|---|
required_status_checks.contexts |
wiz-scan |
Merge blocked while the check is red | The gate |
strict |
true |
Branch must be current before merge | Scan matches the merge result |
enforce_admins |
true |
Admins cannot bypass | Gate binds everyone |
required_pull_request_reviews |
≥1 | Human review alongside the scan | Defense in depth |
| Org ruleset (alternative) | targets main |
Same rule across all repos | Scale beyond one repo |
Architecture at a glance
The flow is a single PR-triggered pipeline with four gates, each backed by a Wiz CI/CD policy, all feeding one console and one graph. Read the diagram left to right. A developer opens a pull request against main. GitHub Actions checks out the code and, using GitHub OIDC, authenticates to HashiCorp Vault to lease the Wiz CI/CD client ID and secret — short-lived, never stored in the repo. The job installs a pinned wizcli, runs wizcli auth to exchange the service-account credential for a session token, and then runs four scans: an IaC scan over the Terraform/CloudFormation/Kubernetes, a secrets/dir scan over the working tree and git history, a Dockerfile scan statically, and — after docker build — an image scan with SBOM extraction over the resolved layers. Each scan enforces its named CI/CD policy; a policy violation makes wizcli exit non-zero, which fails the step, fails the wiz-scan job, turns the required check red, and — via branch protection — blocks the merge.
Every scan uploads its results to the Wiz tenant under a traceable --name, so findings appear in the same console the security team already uses, joined into the code-to-cloud graph where each finding is linked to the cloud resource it becomes and (via the Wiz Sensor, if deployed) the runtime signal that ranks it as a real attack path or noise. In parallel, each scan emits SARIF that GitHub renders as inline annotations on the PR diff and in the Security tab, and the image scan’s CycloneDX SBOM is archived as a workflow artifact for later VEX/admission use. On a clean run the image is pushed and the baton passes to the runtime side — admission control re-checks the digest-bound verdict at the cluster door, agentless CSPM watches for drift introduced outside the pipeline, and the Wiz Sensor plus CrowdStrike Falcon provide runtime threat detection on the live workload. Optionally a failing gate raises a ServiceNow ticket so security has an auditable record, not just a red check that scrolls away.
Real-world scenario
Northwind Freight runs a logistics platform across two clouds: Terraform provisions AWS (the customer-facing API on EKS, an RDS PostgreSQL, a fleet of S3 buckets) and Azure (an internal analytics stack), and every service image builds FROM a shared northwind/base:debian-slim. The platform team is six engineers merging roughly 40 infrastructure and application PRs a week. Security is two people. Before this integration, their control was Wiz’s agentless AWS connector scanning the running estate nightly — good coverage, but everything it found was already deployed, and the two-person security team was drowning in tickets against code that shipped days earlier.
The trigger was an incident. A refactor of the S3 module — extracting a reusable bucket submodule — flipped a default: the new module omitted the aws_s3_bucket_public_access_block resource, and three buckets (one holding customer shipment manifests) went public on merge. Wiz’s nightly scan caught it eleven hours later; by then the buckets had been publicly listable for most of a business day. No data was proven exfiltrated, but the incident review was brutal: the misconfiguration was in the PR, perfectly diagnosable, and nothing looked at the PR. The CISO’s directive: gate it.
They wired Wiz Code into GitHub Actions exactly as this guide describes, but the adoption nearly failed on day one for the classic reason — the first PR after enabling the gate blocked immediately on 214 pre-existing findings, none of which that PR created. The team’s instinct was to disable the gate. Instead they baselined: marked the 214 as the accepted starting set, so the gate failed only on new findings, and scheduled a backlog burn-down of two findings per sprint. Overnight the gate went from “blocks everything” to “blocks only regressions” — and it caught its first real one within three days: a PR that reintroduced the exact public-bucket pattern from the incident. The wiz-scan check went red, the finding annotated line 31 of the module, the developer fixed it in one commit, and the buckets never went public. The security team saw the finding in the same console, joined in the graph to “S3 bucket, would-be-internet-exposed, holds shipment data” — a Critical attack path, not a flat warning.
The vulnerability side taught the second lesson. The shared northwind/base:debian-slim picked up a Critical libssl CVE the week it was disclosed. Because every service builds FROM it, the next image scan on every service PR failed — 40 PRs a week, all red, all on the same unpatchable-that-day CVE. Blocking all of them would have ground the team to a halt. The Block-Critical-Fixable-Vulns policy saved them: the CVE had no fix upstream yet, so it was scoped to warn, not fail — the team saw it, tracked it in a ticket, and merged; when Debian shipped the patch two days later, they rebuilt the base, and the warning cleared everywhere at once. Had the policy blocked on all Criticals rather than fixable Criticals, the team would have disabled the gate under pressure — the exact failure this article warns against.
Six weeks in, the numbers: zero misconfigurations reached production that the gate could have caught (down from a running average of three to four per month), mean-time-to-fix for a caught misconfiguration dropped from eleven hours (nightly-scan-to-ticket-to-fix) to under two minutes (annotation-to-commit), and the two-person security team stopped writing tickets against shipped code and started reviewing .wizignore exceptions instead — a strategic role, not a reactive one. The lesson on the wall: “The gate is only adoptable if it blocks regressions, not debt — baseline first, scope to fixable, and let the graph tell you what’s actually a Critical.”
The incident-to-steady-state timeline, because the order of moves is the lesson:
| Phase | State | Action | Effect |
|---|---|---|---|
| Before | Nightly cloud scan only | (findings after deploy) | 11-hour detection; ticket backlog |
| Incident | 3 buckets public on merge | Caught by nightly scan | Public for most of a business day |
| Day 1 | Gate enabled, 214 findings | First PR blocks on debt | Team nearly disables the gate |
| Day 1 (fix) | Baseline set | Gate on new findings only | Blocks regressions, not debt |
| Day 3 | Public-bucket pattern reintroduced | Gate red, annotation on line 31 | Fixed in one commit; never public |
| Week 2 | Base image gets unfixable Critical | fixable-only policy warns, not fails |
40 PRs merge; CVE tracked |
| Week 2+2d | Debian patches | Rebuild base | Warning clears everywhere at once |
| Week 6 | Steady state | Security reviews exceptions | 0 preventable misconfigs shipped; MTTF < 2 min |
Advantages and disadvantages
Wiz Code’s model — graph-joined, policy-gated, pre-merge scanning — both delivers real value and imposes real constraints. Weigh it honestly:
| Advantages | Disadvantages |
|---|---|
| Findings joined to the code-to-cloud graph — you gate on real attack paths, not flat CVSS, which slashes false-positive fatigue | The graph and prioritization value depend on a Wiz tenant + (ideally) Sensor; sensor-free you lose the runtime-reachability signal |
| One console and one graph for code + cloud + runtime — security sees the PR finding and the running-resource context together | Vendor consolidation on Wiz; less tool-choice flexibility than stitching best-of-breed OSS |
| Policy lives in Wiz, not YAML — security owns the threshold centrally; devs can’t weaken it by editing the workflow | Central policy is a coordination point; a too-strict policy blocks everyone until security relaxes it |
| Multi-format IaC (TF/CFN/K8s/ARM/Helm) + image + secrets + SBOM in one CLI — one gate covers the whole surface | Four scan types add CI minutes and a moving-part surface to maintain |
| SARIF + PR comments put findings on the exact diff line — behavior-changing developer experience | SARIF alone lacks graph context; the richer view needs the Wiz GitHub app installed |
| Baselining makes brownfield adoption humane — block regressions, not pre-existing debt | Baselining can silently accept new debt if you re-baseline carelessly |
| Licensed within the Wiz subscription — marginal cost is mostly CI minutes, not per-scan fees | The Wiz subscription itself is an enterprise-tier cost; not a fit for a hobby project |
| Digest-bound verdicts enable admission-style enforcement — the gate carries past CI to the cluster | Full admission enforcement is additional setup (controller + policy engine) beyond the PR gate |
The model is right for a team with meaningful multi-cloud IaC velocity and container output, an existing (or planned) Wiz investment, and a security team that wants to shift from reactive ticketing to policy ownership. It is over-engineered for a single-repo hobby project (use OSS scanners) and it presumes the Wiz platform is already the estate’s posture tool — bolting Wiz Code onto a shop with no other Wiz footprint loses most of the graph value that justifies it. The disadvantages are all manageable — baseline carefully, scope policies to fixable/graph-rated, pin the CLI, deploy the sensor where you can — but only if you know they exist, which is the point of this article.
Hands-on lab
This lab proves the gate end to end on a real repository: you will reproduce a blocked PR from a public S3 bucket, watch wizcli exit non-zero, see the finding annotate the diff, fix it, and confirm the check flips green — then tear everything down. It is designed to run against your own Wiz tenant with a CI/CD service account. Where a Wiz tenant is not available, Steps 1–3 and 9 (the local validation and teardown) still demonstrate the mechanics; the PR-gate steps need the tenant.
Step 0 — Prerequisites check. Confirm you have a Wiz service account (client ID + secret) and a test GitHub repo you own with Actions enabled.
# You should have these two values from Wiz Settings -> Service Accounts
echo "Client ID present: ${WIZ_CLIENT_ID:+yes}"
echo "Client secret present: ${WIZ_CLIENT_SECRET:+yes}"
Expected: both print yes. If not, create the service account first (Wiz console → Settings → Service Accounts → Add, type CI/CD).
Step 1 — Install wizcli locally.
curl -sSLo wizcli https://wizcli.app.wiz.io/latest/wizcli
chmod +x wizcli
sudo mv wizcli /usr/local/bin/wizcli
wizcli version
Expected: a version string prints (e.g. wizcli version 1.x.y). If curl fails, check egress to wizcli.app.wiz.io.
Step 2 — Authenticate.
wizcli auth --id "$WIZ_CLIENT_ID" --secret "$WIZ_CLIENT_SECRET"
Expected: Authentication successful (or equivalent) and a cached session. A non-zero exit here means bad credentials or clock skew — fix before continuing; do not proceed treating auth failure as “clean.”
Step 3 — Reproduce a blocked IaC finding locally. Create an obviously-bad Terraform file — a public S3 bucket — and scan it against your IaC policy:
mkdir -p /tmp/wizlab/infra
cat > /tmp/wizlab/infra/main.tf <<'EOF'
resource "aws_s3_bucket" "leak" {
bucket = "kv-wizlab-leak-bucket"
}
resource "aws_s3_bucket_public_access_block" "leak" {
bucket = aws_s3_bucket.leak.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
EOF
wizcli iac scan --path /tmp/wizlab/infra --policy "Block-Critical-High-IaC"; echo "exit=$?"
Expected: a Critical/High finding for public S3 exposure and a non-zero exit=. That non-zero exit is the entire gate mechanism — it is what will fail the GitHub step. If it exits 0, your policy is not gating on this class; check the policy’s severity threshold in the Wiz console.
Step 4 — Add the workflow to your repo. In your test repo, create .github/workflows/wiz-code-gates.yml. For the lab, use the simpler encrypted-secret path (skip the Vault step) — set WIZ_CLIENT_ID and WIZ_CLIENT_SECRET under Settings → Secrets and variables → Actions, and use this trimmed workflow:
name: Wiz Code Security Gates
on:
pull_request:
branches: [main]
permissions:
contents: read
security-events: write
jobs:
wiz-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install wizcli
run: |
curl -sSLo wizcli https://wizcli.app.wiz.io/latest/wizcli
chmod +x wizcli && sudo mv wizcli /usr/local/bin/wizcli
- name: Auth
run: wizcli auth --id "${{ secrets.WIZ_CLIENT_ID }}" --secret "${{ secrets.WIZ_CLIENT_SECRET }}"
- name: IaC scan
run: |
wizcli iac scan --path ./infra \
--name "wizlab-${{ github.sha }}" \
--policy "Block-Critical-High-IaC" \
--output sarif,wiz-iac.sarif,true
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: wiz-iac.sarif
category: wiz-iac
Commit this to main.
Step 5 — Make the check required.
gh api -X PUT repos/<you>/<repo>/branches/main/protection \
-f 'required_status_checks[strict]=true' \
-f 'required_status_checks[contexts][]=wiz-scan' \
-F 'enforce_admins=true' \
-F 'required_pull_request_reviews=null' \
-F 'restrictions=null'
Expected: JSON confirming protection is set with wiz-scan as a required context.
Step 6 — Open a PR that introduces the bad bucket. On a branch, add the same infra/main.tf from Step 3, push, and open a PR against main.
git checkout -b add-bad-bucket
mkdir -p infra && cp /tmp/wizlab/infra/main.tf infra/main.tf
git add infra/main.tf && git commit -m "add bucket (intentionally bad for lab)"
git push -u origin add-bad-bucket
gh pr create --fill --base main
Expected on the PR:
| Check | Expected state |
|---|---|
wiz-scan status check |
Red / failed |
| Finding on the diff | Inline annotation on the public_access_block lines |
| Merge button | Blocked (“Required check failing”) |
| Security → Code scanning tab | The public-bucket finding listed |
Step 7 — Fix it in the same PR. Flip the booleans to true and push:
sed -i.bak 's/= false/= true/g' infra/main.tf && rm -f infra/main.tf.bak
git commit -am "fix: block public access on the bucket"
git push
Expected: the workflow re-runs, wizcli iac scan exits 0, the wiz-scan check flips green, and the merge unblocks.
Step 8 — Confirm the console and (if wired) the SBOM. In the Wiz console, find the scan under the name wizlab-<sha> and confirm the finding was recorded and then resolved. If you added the image-scan step, confirm the SBOM artifact is attached to the workflow run.
Step 9 — Teardown. Remove the gate and clean up, in reverse order of how you built it:
# Drop wiz-scan from required checks
gh api -X PUT repos/<you>/<repo>/branches/main/protection \
-f 'required_status_checks[strict]=true' \
-F 'required_status_checks[contexts][]=' \
-F 'enforce_admins=true' -F 'required_pull_request_reviews=null' -F 'restrictions=null'
# Disable the workflow (keeps the file, stops runs)
gh workflow disable "Wiz Code Security Gates" -R <you>/<repo>
# Local cleanup
rm -rf /tmp/wizlab
Finally, in the Wiz console delete or disable the CI/CD service account so the now-unused credential cannot be replayed — deleting the workflow file alone is not enough; always revoke the credential. If you used the Vault path, also vault delete auth/github-actions/role/wiz-scan and vault kv metadata delete secret/ci/wiz.
Common mistakes & troubleshooting
Each of these is a real failure mode with a distinct fingerprint. Match the symptom, confirm the cause, apply the fix:
| # | Symptom | Root cause | Confirm | Fix |
|---|---|---|---|---|
| 1 | Job dies before any scan with a 403 | Missing id-token: write; OIDC handshake to Vault fails |
Workflow log shows OIDC/JWT 403 at the Vault step | Add id-token: write to permissions: |
| 2 | Findings never appear on the PR | Missing security-events: write; SARIF upload no-ops |
Upload step “succeeds” but Security tab empty | Add security-events: write; check category |
| 3 | Check goes red but merge still works | Not a required check, or enforce_admins=false |
Branch protection shows wiz-scan not required |
Make it required; enforce_admins=true |
| 4 | Only Dockerfile scanned; CVEs slip through | Skipped the image scan; static-only | No wizcli docker scan --image step present |
Add the post-build image scan |
| 5 | Every PR blocks on day one | Gating on pre-existing debt, no baseline | 200+ findings, none new to the PR | Baseline the current set; gate on new only |
| 6 | Team routes around the gate | Blocking on unfixable Critical CVEs | Failing CVE has no upstream patch | Scope vuln policy to fixable Criticals |
| 7 | Auth failure treated as “clean” | Step swallows non-zero auth exit | wizcli auth failed but job passed |
Fail fast on auth error; never continue |
| 8 | Scan passes but shouldn’t | --policy name typo → wrong/no policy applied |
Console shows scan ran with no policy | Match the exact policy name from Wiz |
| 9 | Computed value misconfig missed | Scanned raw HCL, not the plan | Bad value is a Terraform variable/expression | Scan terraform show -json plan output |
| 10 | Secret “removed” but still leaked | Deleted the commit, didn’t rotate | Key still valid against the cloud provider | Rotate the key; purge history |
| 11 | Intermittent scan failures | wizcli@latest changed exit semantics |
Failures started after a CLI auto-update | Pin WIZCLI_VERSION to a known-good version |
| 12 | .wizignore silently hollows the gate |
Un-reviewed, never-expiring ignores accumulate | Many entries, no reasons/expiries | CODEOWNERS .wizignore → security; lint expiries |
| 13 | Later scans skipped after one fails | All scans in one run: with set -e |
First failure aborts the shell step | One scan per step (see exemplar YAML) |
| 14 | Image scanned ≠ image shipped | Scanned a mutable tag, pushed a different build | Digest in scan ≠ digest pushed | Scan by digest; bind verdict to it |
| 15 | Gate blocks during a Wiz outage | Fail-closed with Wiz unreachable | wizcli network error, all PRs red |
Expected — use a logged, time-boxed break-glass only |
The most common single mistake by far is #5 / #6 — turning the gate on at full strictness against a brownfield repo. The gate is technically working; it is socially failing, because it blocks debt no PR created (fix with baselining) or blocks CVEs no developer can patch (fix with fixable-scoping). Get those two right and adoption succeeds; get them wrong and the team disables the gate within a week, which is worse than no gate because it teaches that security controls are obstacles.
Best practices
- Baseline before you enforce. On any existing repo, mark current findings as the accepted baseline and gate only on new findings a PR introduces. Block regressions, not pre-existing debt; burn the backlog down on a schedule.
- Scope vulnerability gates to fixable Criticals. Blocking on unpatchable CVEs teaches developers to route around the gate. Fail on Critical-with-a-fix (and malware, always); warn on unfixable, track via VEX.
- Own the policy in Wiz, not in YAML. Keep the pass/fail threshold in a named CI/CD policy so security can tighten it without a code change and developers cannot weaken it by editing the workflow.
- Run each scan as its own step. With
set -einrun:blocks, one failing scan aborts a combined step and hides later scans. Separate steps let all four run and all four annotate. - Scan the Dockerfile and the built image. Static smells (
:latest,ADDfrom URL, secrets inENV) and resolved CVEs are different finding sets; run both passes. - Scan by digest and bind the verdict. Scan the exact image digest you will ship so the PR verdict, the console record, and any admission check reference the same immutable artifact.
- Lease the credential via OIDC. Prefer OIDC → Vault (or a cloud secrets manager) with a short TTL and repo/ref-bound claims over a static GitHub secret, so nothing long-lived lives in the repo.
- Least-privilege the service account. Scope it to CI/CD scanning only; it never needs your full cloud-inventory read. Rotate on a schedule; delete it when you tear the pipeline down.
- Make the check required and bind admins. A gate that admins can bypass is bypassed under deadline pressure.
enforce_admins=true,strict=true, and use org rulesets to apply it across repos. - Treat
.wizignoreas reviewed code. Reason + expiry on every entry, CODEOWNERS to the security team, and a CI lint that flags expired entries. Fix beats ignore; every ignore is tracked debt. - Pin the CLI version in production. A
latestCLI can change exit semantics mid-stream. Validate a version, pin it, and upgrade deliberately. - Carry the gate past CI. Pair the PR gate with registry scanning, admission control, agentless CSPM for drift, and a runtime sensor. Shift-left is the first layer, not the only one.
- Fail closed. A scan that could not run is not a scan that passed. Fail-open is only a logged, time-boxed break-glass during a known Wiz outage.
- Deploy the Wiz Sensor where you can. It feeds runtime-reachability into the graph, which is what turns “30 Criticals” into “3 real attack paths” — the prioritization that makes the whole thing scale.
Security notes
The entire premise is shift-left: stop the misconfiguration and the vulnerable image at the pull request, before they reach a cloud account, become reachable, or enter the code-to-cloud graph as a live attack path. But pre-merge scanning is the first control, not the only one, and treating it as complete is itself a risk. Three security properties deserve emphasis.
First, credential hygiene is as important as the scan. Leasing the Wiz secret from HashiCorp Vault via GitHub OIDC with a short TTL, and binding issuance to your repo and PR refs via bound_claims, means there is no static, long-lived Wiz key in GitHub to leak, and a fork cannot mint the credential. The service account is scoped to CI/CD scanning only — the pipeline never needs (and must never hold) your full cloud-inventory scope, because a leaked scanning credential should not be a leaked estate.
Second, a secret found in a PR is already compromised. The gate blocking a verified secret is the detection; the response is rotation, not deletion. A key that was pushed to GitHub’s servers is exposed regardless of whether you later remove the commit — assume it is public, rotate it, then purge history. This is why the secrets policy is the strictest (fail on any verified secret) and why it dovetails with a dedicated remediation runbook.
Third, the runtime layers stay on the job after the artifact ships. Wiz continues agentless CSPM of the running estate so drift introduced outside the pipeline (a console edit, a break-glass change, a resource created by another tool) is still caught — the PR gate governs code-driven change, not every change. Admission control re-checks the digest-bound verdict at the cluster door so an image that skipped CI is stopped at deploy. And a runtime sensor (the Wiz Sensor, and/or CrowdStrike Falcon) provides threat detection on the live workload — the layer that watches for exploitation of whatever did ship. The security posture is the whole stack; Wiz Code is the earliest, cheapest layer, not a substitute for the rest.
The security-control layering, and what each is authoritative for:
| Control | Authoritative for | Blind to |
|---|---|---|
| Wiz Code PR gate | Code-driven misconfig/vuln/secret pre-merge | Drift outside the pipeline; post-deploy CVEs |
| Registry scan | Vulns discovered after build, on stored images | Images never pushed to that registry |
| Admission control | Unscanned/failing images at the cluster door | Non-K8s workloads |
| Agentless CSPM | The running cloud posture + drift | In-workload runtime behavior |
| Runtime sensor (Wiz/Falcon) | Exploitation of the live workload | Pre-deploy issues (that’s the PR gate’s job) |
Cost & sizing
Wiz Code scanning is licensed within your Wiz subscription, so the marginal cost of adding these gates is dominated not by per-scan fees but by CI minutes and the (fixed) Wiz platform cost you already carry. The per-PR budget breaks down roughly as follows — the numbers are indicative and vary with image size and repo:
| Cost component | Typical magnitude | Driver | Reduce it by |
|---|---|---|---|
| IaC scan | ~10–40 s | Number of IaC files | Point --path at the IaC dir, not the whole repo |
| Secrets/dir scan | ~15–60 s | Repo + history size | fetch-depth tuned; scan history less often |
| Dockerfile scan | ~5–15 s | Trivial (static) | Negligible; keep it |
docker build |
1–5 min | Image size + layer cache | Layer caching (docker/build-push-action, cache) |
| Image scan (+ SBOM) | ~20–90 s | Image size + package count | Smaller base image; fewer packages |
| Total added per PR | ~2–7 min | Dominated by docker build, not the scans |
Cache the build; trigger only on PR-to-main |
The economics favor the gate heavily. A few CI minutes per PR is trivial against the fully-loaded cost of a misconfiguration discovered in production — the incident bridge, the change-freeze, the forensics if it was reachable, the rebuild and redeploy, and the reputational and regulatory exposure of, say, a public bucket of customer data. Northwind’s scenario put a number on the time saved (11 hours → under 2 minutes mean-time-to-fix per caught finding); the cost saved is the incident that never happens.
Keep the CI bill small with three levers: trigger scope (run on pull_request to main only, not every push to every branch — this is the single biggest saver), build caching (the scans are cheap; the docker build they depend on is not — cache layers and Terraform providers so the build is fast), and single-job execution (run all four scans in one job to avoid re-checkout and re-auth overhead across jobs). Rough figures: on GitHub-hosted ubuntu-latest runners, an added ~3–5 minutes per PR at 40 PRs/week is ~10–13 hours of runner time monthly — well within a standard Actions allowance, and negligible against even one avoided incident. Self-hosted runners drop the marginal minute cost to near zero if you already run a fleet; the trade-off is maintaining the runners (see the self-hosted-runner economics in your CI platform’s sizing).
Interview & exam questions
Q1. Why scan the Dockerfile and the built image rather than just one?
The Dockerfile scan is static and pre-build — it catches :latest base pins, ADD from a URL, secrets in ENV, and a missing non-root USER before any build cost. The image scan runs after docker build and inspects the resolved layers — the actual base the tag pointed to and the OS/language CVEs that apt/pip/npm pulled in. A clean-looking Dockerfile can resolve to a base carrying a Critical CVE; they are complementary finding sets, so you run both.
Q2. What actually makes the pipeline block a merge, mechanically?
wizcli … --policy <name> evaluates the scan against a named Wiz CI/CD policy and exits non-zero when the policy is violated. That non-zero exit fails the GitHub step, fails the job, turns the wiz-scan required status check red, and — with branch protection making it required and enforce_admins=true — GitHub refuses the merge. The exit code is the enforcement; the policy is the contract.
Q3. Why fail on fixable Critical CVEs rather than all Criticals? Blocking a build on a Critical with no available patch gives the developer no action to take, so the gate becomes an obstacle they learn to route around — which destroys the control. Scoping the vuln policy to fixable Criticals (plus malware, always) keeps every block actionable; unfixable Criticals are surfaced as warnings and tracked via VEX until a patch ships.
Q4. A team turns the gate on and every PR blocks on day one. What went wrong and how do you fix it? The gate is enforcing against pre-existing findings that no individual PR created — brownfield debt. The fix is baselining: mark the current findings as the accepted starting set so the gate fails only on new findings a PR introduces, then burn the backlog down on a schedule. Block regressions, not debt.
Q5. Explain how the code-to-cloud graph changes prioritization versus a flat CVSS list. A flat scanner produces a list keyed on CVE severity, which drowns teams in “Criticals” that are unreachable or theoretical. The graph joins each finding to the cloud resource it becomes, that resource’s network exposure, the identity it can assume, the data it can reach, and (via the sensor) whether it is actually running and loaded — so the gate can distinguish a genuine internet-exposed attack path from an unreachable theoretical CVE, and gate accordingly.
Q6. What does the Wiz Sensor add, and is it required for the PR gate? The Wiz Sensor is an eBPF runtime agent that feeds “is this actually running / loaded / exposed” signal into the graph, which sharply improves prioritization (a CVE in never-loaded code is deprioritized; one in a loaded, exposed, privileged workload is escalated). It is not required to gate PRs — Wiz Code scans and enforces policy without it — but where present it turns raw findings into ranked attack paths.
Q7. Why lease the Wiz credential via OIDC instead of a GitHub secret? A static GitHub secret is long-lived material sitting in the platform; if the repo or the secret store is compromised, the key leaks. OIDC leases the credential at run time from a broker (Vault or a cloud secrets manager) with a short TTL and claims bound to your repo and PR refs, so nothing long-lived lives in GitHub and a fork cannot mint it. The Wiz secret never touches the repo.
Q8. A secret is caught in a PR. Is deleting the commit sufficient? No. A secret pushed to GitHub’s servers is compromised the moment it lands — it may be in forks, caches, and the reflog. Deleting the commit removes it from the diff but not from exposure. The correct response is to rotate the key (invalidate it at the provider), then purge history. Detection is the gate’s job; rotation is the response.
Q9. Why scan the image by digest rather than by tag? A tag is mutable — the tag you scanned in CI can point to a different build than the one you push, so the verdict would not describe the shipped artifact. Scanning by digest binds the scan to the exact immutable image, so the PR verdict, the Wiz console record, and any admission-time re-check all reference the same bytes.
Q10. What is admission-style enforcement and what gap does it close? CI gates code-driven change at the PR, but images can reach a cluster by paths that skipped CI, and stored images grow new CVEs after they were built. Admission control re-checks the digest-bound Wiz verdict at deploy time and refuses to admit a pod whose image has not passed policy — closing the gap between “passed CI once” and “is being deployed now.”
Q11. Where does Wiz Code stop and runtime tools take over? Wiz Code is the pre-merge, code-security layer — it stops misconfig/vuln/secret findings in the PR. Once the artifact ships, agentless CSPM watches the running posture for drift introduced outside the pipeline, admission control gates deploys, and runtime sensors (the Wiz Sensor and/or CrowdStrike Falcon) detect exploitation of the live workload. Shift-left moves detection earlier; it does not replace the layers that watch what runs.
Q12. How do you keep .wizignore from hollowing out the gate?
Require a reason and an expiry on every entry, put the security team on CODEOWNERS for the file so devs cannot self-approve weakening the gate, add a CI lint that flags expired or expiring entries, prefer narrow paths over broad globs, and track every ignore as debt to be fixed rather than a permanent suppression. Fix beats ignore.
Quick check
- Which
wizcliflag turns a scan into a gate — i.e., makes the CLI exit non-zero on a violation? - You see a red
wiz-scancheck but the PR still merges. Name the two most likely misconfigurations. - Why does the recommended vulnerability policy fail on fixable Criticals but only warn on unfixable ones?
- A brownfield repo blocks every PR on day one. What is the mechanism that makes the gate adoptable?
- Name the runtime component that feeds reachability context into the graph, and state whether it is required to gate PRs.
Answers
--policy "<name>". It evaluates the scan against a named Wiz CI/CD policy and exits non-zero when that policy is violated; the non-zero exit fails the step and the job.- The check is not marked required in branch protection, and/or
enforce_admins=falseso admins bypass it. Make it a required status check and setenforce_admins=true. - Blocking on an unfixable Critical gives the developer no action to take, so they learn to route around the gate; scoping to fixable Criticals keeps every block actionable while unfixable ones are tracked as warnings/VEX.
- Baselining — mark the current findings as the accepted starting set so the gate fails only on new findings a PR introduces, blocking regressions rather than pre-existing debt.
- The Wiz Sensor (eBPF runtime agent). It is not required to gate PRs — Wiz Code scans and enforces policy without it — but it sharply improves prioritization by adding runtime reachability to the graph.
Glossary
- Wiz Code — Wiz’s ASPM / code-security module: the
wizcliscanner, CI/CD policies, and the code side of the security graph. wizcli(wiz-cli) — The single static-binary scanner run on the CI runner; its exit code is the gate.- Service account — A non-human Wiz identity (client ID + secret) the pipeline authenticates as, scoped to CI/CD scanning.
- CI/CD policy — A named Wiz rule deciding pass/fail by severity and finding class; invoked with
--policyand enforced via exit code. - Security Graph (code-to-cloud graph) — Wiz’s joined model of code artifacts, the cloud resources they become, exposure, identity, data, and runtime signal.
- Attack path — A chain in the graph from an exposure through identity to impactful data; the unit of real prioritization, not raw CVSS.
- Wiz Sensor — An eBPF runtime agent that feeds “is it running/loaded/exposed” context into the graph; optional for scanning, powerful for prioritization.
- SARIF — Static Analysis Results Interchange Format; GitHub renders uploaded SARIF as inline PR annotations and Security-tab findings.
- SBOM — Software Bill of Materials (CycloneDX or SPDX); the resolved dependency inventory, used for VEX triage and admission verification.
- CycloneDX / SPDX — The two dominant SBOM formats; CycloneDX is security/VEX-oriented, SPDX is license/ISO-standard-oriented.
- VEX — Vulnerability Exploitability eXchange; a statement of whether a listed vulnerability is actually exploitable in a given product.
- Baseline — The accepted starting set of findings; the gate fails only on findings introduced after the baseline.
.wizignore— A repo-root file of documented, time-boxed exceptions (reason + expiry per entry), reviewed like code.- Policy ignore rule — A Wiz-console suppression of a finding class org-wide, for systematic false positives or accepted patterns.
- Admission control — Deploy-time enforcement that re-checks the digest-bound Wiz verdict and refuses non-compliant images at the cluster door.
- Agentless CSPM — Continuous cloud-posture scanning of the running estate, catching drift introduced outside the pipeline.
- Fail-closed / fail-open — Whether a scan that could not run blocks (closed, the secure default) or allows (open, a time-boxed break-glass only) the merge.
- Digest binding — Scanning and recording a verdict against an image’s immutable digest rather than a mutable tag, so every layer references the same bytes.
Next steps
- Extend the estate-side posture: Roll Out Wiz CSPM Across a Multi-Account AWS Organization with the AWS Connector — the running-cloud scanning that shares this guide’s graph and catches drift outside the pipeline.
- Close the secrets loop: Eliminating Secret Sprawl: Pipeline Scanning, Push Protection, and Leaked-Credential Remediation — the response playbook for a secret the gate catches (rotate, don’t just delete).
- Consume the SBOM you now produce: Consuming the Software Supply Chain: SBOM Ingestion, VEX Triage, and Admission Verification — turn the CycloneDX artifact into VEX-driven admission decisions.
- Harden the credential path: Secretless CI/CD: Workload Identity Federation for GitHub Actions and AKS and CI/CD Secrets and Credential Management: Secure Your Pipelines — the OIDC and secrets-broker patterns behind the Vault step.
- Complete the runtime layer: Deploy CrowdStrike Falcon Sensor to Linux Fleets and Kubernetes via Helm DaemonSet — the threat-detection layer that watches what ships after the gate lets it through.