What Shift-Left Security Scans Miss
Running security scanners in CI is the right default. Problems found in a pull request are cheap to fix. The trouble starts when a green pipeline is read as "the system is secure". A CI scan answers a narrow question: did this commit, at this moment, match known bad patterns in the files the scanner was pointed at?
This post lists what that question leaves out and what to add.
What early scans actually see
- SAST reads source code and finds risky patterns: injection, unsafe deserialisation, weak crypto calls.
- Dependency scanning (SCA) reads lockfiles and matches package versions against advisories.
- Image scanning inspects the built image: OS packages and language packages.
- IaC scanning reads Terraform, Kubernetes manifests, Helm charts and Dockerfiles for risky settings.
- Secret scanning looks for credentials in the diff.
All of these work on artifacts at build time. That defines their blind spots.
Gap 1: time
An image that was clean on the day it was built is not clean a month later. New CVEs are published against packages that did not change. If nobody rebuilds or rescans, nobody notices.
Generate an SBOM at build time and rescan it on a schedule against fresh advisory data:
syft registry.example.com/my-app:1.4.2 -o cyclonedx-json > sbom.json
grype sbom:./sbom.json --fail-on high
Run the rescan nightly for every image that is actually deployed, not for every image ever built. The list of deployed images can come from the cluster (kubectl get pods -A -o jsonpath='{..image}') or from your deploy records.
Gap 2: drift
The IaC scanner sees the repository. It does not see a security group opened in the console, a kubectl edit during an incident, or a resource created by hand and never imported. Detect drift by running plan on a schedule:
terraform plan -detailed-exitcode -lock=false
# exit code 0: no changes, 2: drift or pending changes, 1: error
For cloud accounts, also run a posture tool against the live account: AWS Config rules, Security Hub, or an open source scanner such as Prowler. These check what exists, regardless of how it was created.
Gap 3: what is actually admitted
The manifest in Git is not always what runs. Helm values are overridden per environment, operators create pods, and people deploy from laptops. Enforce rules at admission, where every pod has to pass:
- Pod Security Admission labels on namespaces (
restrictedfor application workloads). - ValidatingAdmissionPolicy, Kyverno or Gatekeeper for registry allowlists, required labels and image signature checks.
- kube-bench to check nodes and control plane settings against the CIS Kubernetes Benchmark.
Gap 4: logic and authorisation
SAST is weak at finding missing authorisation checks. An endpoint that returns another customer's invoice when you change an ID in the URL looks like correct code to a pattern matcher. Cover this with:
- tests that call endpoints as user A and assert that user B's objects return 403 or 404;
- a dynamic scan of a running staging environment, for example the ZAP baseline scan:
docker run --rm -t ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py -t https://staging.example.com
The baseline scan is passive and safe to run against staging regularly. Active scans need an agreed window and should never point at production without approval.
Gap 5: scope
Scanners only scan what they are configured to scan. Common holes: a second repository with deployment code, a monorepo path excluded "temporarily", base images built in another pipeline, Helm charts pulled from a public registry, and Terraform modules referenced by Git URL. Keep an inventory of what gets deployed and check that every entry is covered by at least one scan.
Gap 6: the toolchain itself
Scanners and CI actions are dependencies with access to your pipeline secrets. In March 2026 the tags of the aquasecurity/trivy-action and setup-trivy GitHub Actions were force-pushed to malicious commits that stole CI secrets (advisory GHSA-69fq-xp46-6x23, CVE-2026-33634; the same incident also shipped a malicious trivy v0.69.4 release and Docker images). Workflows that referenced actions by tag picked up the malicious code without any change on their side.
- Pin third-party actions to a full commit SHA, not a tag.
- Pin scanner binary versions and verify checksums.
- Give scan jobs the minimum permissions and no deploy credentials.
Gap 7: noise
A scanner that reports thousands of findings gets ignored. Fail the build on what is new and important, track the rest separately:
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 \
registry.example.com/my-app:1.4.2
trivy config --severity HIGH,CRITICAL --exit-code 1 ./infra
trivy config is also the replacement for tfsec, which was merged into Trivy. Accepted risks belong in an ignore file with an owner and an expiry date, not in a disabled check.
A few corrections to common examples
- An
aws_inspector_assessment_targetresource does not scan Terraform. It configures Inspector Classic assessments of running EC2 instances. For IaC, run an IaC scanner such as Trivy or Checkov in CI. fsGroupis a pod-level setting. Inside a container'ssecurityContextit is invalid.image: my-app:latestmakes scan results meaningless, because the scanned image and the running image may differ. Deploy by digest or immutable tag.
Checklist
- SAST, SCA, image, IaC and secret scans in CI, failing on new high-severity findings.
- SBOM per image, nightly rescan of deployed images.
- Scheduled
terraform plan -detailed-exitcodeand a cloud posture scan. - Admission control in every cluster, kube-bench on nodes.
- Authorisation tests and a passive DAST scan against staging.
- Inventory of deployed components mapped to scans.
- Actions pinned by SHA, scanner versions pinned, scan jobs without deploy credentials.
