Everything for Checkmarx in one place — pick a section below. 11 reviewed items across 4 content types.
Quick Answer
SAST engines trace 'taint' — untrusted data — from where it enters the program (a source, like an HTTP request parameter) through every function call and assignment it passes through, until it either reaches a dangerous operation (a sink, like a raw SQL query) or gets neutralized along the way. False positives on sanitized input happen when that sanitization occurs inside a shared library, framework helper, or custom utility function the analyzer either doesn't recognize as a sanitizer or can't fully trace into, so it conservatively still flags the path as tainted even though the input was actually made safe.
Detailed Answer
Picture tracking a rumor as it spreads through an office to see if it ever reaches someone who'd act on it recklessly — you'd trace it person by person: who told whom, who added details, whose retelling actually changed the story enough that it stopped being the same claim. A SAST engine's taint analysis works exactly like tracing that rumor: it marks data as 'tainted' the moment it originates from an untrusted source (a user-submitted form field, a URL parameter, an uploaded file), and then follows that taint through every subsequent variable assignment, function call, and transformation in the code, the same way you'd track whether the original rumor is still recognizably present in what someone repeats three conversations later.
This rumor-tracing model is precisely how static taint analysis was designed to catch injection vulnerabilities without ever running the program: if tainted data flows, unmodified or insufficiently modified, all the way into a dangerous sink — a raw SQL query string, a shell command, an HTML response written without encoding — that's flagged as a potential injection vulnerability, because an attacker who controls the source (the original untrusted input) effectively controls what happens at the sink. The entire value proposition of SAST is catching this class of bug before the code ever runs anywhere, which DAST and manual testing simply cannot do as exhaustively, since they can only observe behavior along paths they actually happen to execute.
Step by step, the engine builds this as a graph: nodes represent variables, function calls, and expressions; edges represent data flowing from one to another. Starting from every known 'source' (a catalog the engine maintains of framework-specific entry points for untrusted data — request parameters, headers, cookies, database read results in some threat models), it performs a reachability analysis across that graph to every known 'sink' (SQL execution calls, command execution calls, raw HTML output). Along the way, if the data passes through a recognized 'sanitizer' (a parameterized query binding, an established escaping function, an allow-list validation the engine's rule set knows about), the taint is cleared and that path is no longer flagged. Crucially, this entire process happens without executing the code — it's purely a graph traversal over the program's structure, which is exactly why it can analyze every possible path through the code, including ones that would be extremely rare or hard to trigger in a live running system, but also exactly why it has no actual runtime context to know for certain whether a given path is truly reachable in practice.
At production scale, the false-positive pattern the question describes — sanitized input still getting flagged — happens because the engine's built-in catalog of 'known sanitizer' functions is necessarily finite and mostly covers common, well-known frameworks. A custom, internal validation utility that a company built in-house (say, a shared InputSanitizer.clean() helper used company-wide) isn't in Checkmarx's default sanitizer catalog, so from the engine's point of view, tainted data going into that function and coming back out is exactly as tainted as it was going in, because the engine has no built-in knowledge that this particular function actually neutralizes the risk — it just sees a function call, not a security control. The data flow through that custom sanitizer looks structurally identical to data flowing through any other unremarkable function, so the engine, erring conservatively (correctly preferring false positives over false negatives for a security tool), keeps the taint alive and flags the eventual sink.
The gotcha, and the actual fix experienced AppSec engineers reach for: Checkmarx supports custom sanitizer definitions specifically to solve this, letting a team explicitly register their in-house validation function as a recognized taint-clearing point, which permanently eliminates this entire class of false positive going forward rather than requiring a human to manually re-triage the same false positive on every single scan, forever, across every service that uses the same shared utility. Teams that never configure custom sanitizers end up in a chronic, compounding state where the same handful of company-wide utility functions generate the same false positives across dozens of services indefinitely, training developers to reflexively dismiss SAST findings as noise — which is the exact security-fatigue failure mode that eventually lets a genuinely new, real finding get rubber-stamped away along with all the familiar false ones.
Code Example
# Checkmarx custom sanitizer definition (CxQL) — teach the engine your in-house helper clears taint
// Registers InputSanitizer.clean() as a recognized sanitizer for SQL injection queries
result = SQL_Injection.Sinks()
.NewSanitizer(All.Cast(TypeName("InputSanitizer")).GetMethodsByName("clean"))
.Sinks();
# Run a scan and specifically inspect the taint path for a flagged finding
cx scan create --project-name payments-api --source . --scan-types sast
cx results show --scan-id <scan-id> --filter "query-name=SQL_Injection" --format json | jq '.dataFlow'
# Mark a confirmed false positive at the specific finding level (not a blanket suppression)
cx triage update --project-id <id> --similarity-id <sim-id> --state "NOT_EXPLOITABLE" \
--comment "Input passes through company InputSanitizer.clean(), registered as custom sanitizer"Interview Tip
A junior engineer typically dismisses SAST false positives as 'the tool just isn't smart enough,' but for a senior role the interviewer is actually looking for the mechanistic reason — the engine's sanitizer catalog is necessarily finite, and a custom in-house validation function looks structurally identical to any other unremarkable function call unless it's explicitly registered as a recognized sanitizer. The strongest answers know the actual fix is defining custom sanitizers (in Checkmarx, via CxQL) rather than just manually re-triaging the same false positive scan after scan, and explain why that manual-dismissal pattern, repeated indefinitely across many services, is exactly what trains developers to stop trusting SAST output altogether — which is the real security cost of not investing in tuning the tool properly.
◈ Architecture Diagram
source (user input) ──▶ InputSanitizer.clean() ──▶ SQL query (sink)
│
engine doesn't recognize this
as a sanitizer by default
│
↓
✗ false positive (until registered as custom sanitizer)💬 Comments
Quick Answer
First confirm whether the codebase actually changed or only the scan engine/ruleset version did, since a sudden spike immediately after an engine update with no corresponding code change points strongly at new or modified detection rules rather than new real vulnerabilities. Compare the new findings' query IDs against the engine's release notes for newly added or changed rules, and spot-check a sample of the flagged data-flow paths manually before deciding whether to bulk-suppress, bulk-fix, or roll back the engine version while you evaluate.
Detailed Answer
This is like a building's fire inspection suddenly failing on hundreds of new violations the day after the fire code itself was rewritten, even though nothing about the building changed overnight. The natural, wrong instinct is to assume the building suddenly became unsafe; the actually useful first question is whether the building changed or the rulebook changed, because those require completely different responses — one means real, urgent remediation, the other means understanding and adapting to updated standards, possibly along with recalibrating which of the new rules genuinely apply to your situation versus which are overly broad interpretations that need tuning.
Checkmarx and every SAST vendor periodically ship engine updates that add new detection queries, tighten existing ones, or update their built-in sanitizer and sink catalogs — this is a genuinely necessary process, since new vulnerability classes and attack techniques are discovered continuously and a static tool that never updates its rules becomes stale and increasingly blind to current threats. But the practical consequence is that an engine update can retroactively 'discover' large numbers of new findings in code that hasn't changed at all, simply because the tool now looks for patterns it didn't check for last week — and treating every one of those as an urgent new production risk, at the same priority as a finding introduced by an actual code change, misallocates security team attention.
Step by step: first, establish the timeline precisely — pull the CI pipeline history and confirm whether the code diff between the last passing build and this failing one is trivial or substantial; if the actual application code is nearly unchanged but the Checkmarx CLI or scan engine image version bumped in the same window, that's your leading signal this is a ruleset change, not a code regression. Second, cross-reference the new findings' query names or IDs against the vendor's release notes for that engine version — Checkmarx publishes what queries were added, modified, or had their default severity changed in each release, and a spike concentrated in queries that line up with the release notes confirms the hypothesis directly rather than leaving it as a guess. Third, before bulk-dismissing anything, manually spot-check a representative sample of the new findings' actual data-flow paths — some fraction of a newly-tightened rule's findings are often genuinely real issues the previous, looser version of the rule simply never caught, so a real regression can be hiding inside what's mostly ruleset noise, and blanket-dismissing the whole batch risks missing that real subset. Fourth, decide on an interim strategy: options include temporarily pinning the previous engine version while the team properly triages the new ruleset's findings on its own schedule, or keeping the new engine active but adjusting the pipeline's policy gate threshold specifically for the newly-added query IDs while the backlog gets worked through, rather than blocking every single deploy in the meantime.
At production scale, mature AppSec teams treat scan-engine and ruleset upgrades as a change that itself needs a rollout process, not something that silently takes effect and immediately gates every team's CI pipeline at full strictness — testing an engine upgrade against a representative sample of existing codebases in a non-blocking, report-only mode before promoting it to a hard gate lets the team absorb exactly this kind of finding-count spike deliberately and on their own timeline, rather than discovering it for the first time when it unexpectedly blocks every team's deploys simultaneously.
The gotcha: teams under deploy-pressure often respond to a sudden spike by immediately, blanket-suppressing every new finding just to unblock the pipeline, reasoning that 'it's probably just the new ruleset' without actually verifying that — and this is precisely how a real, newly-introduced vulnerability that happened to land in the same release window as an engine update gets silently waved through as assumed noise, discovered only much later, if ever, when it's actually exploited rather than caught at the gate it was specifically designed to be caught at.
Code Example
# Step 1: confirm whether app code or scan engine version actually changed
git log --oneline -- src/ | head -5 # did application code change meaningfully?
cx --version # compare against last known passing pipeline run
# Step 2: pull findings grouped by query ID to compare against engine release notes
cx results show --scan-id <new-scan-id> --filter "severity=HIGH" --format json | \
jq -r '.results[].queryName' | sort | uniq -c | sort -rn
# Step 3: diff findings against the previous scan to isolate genuinely NEW findings
cx results show --scan-id <previous-scan-id> --format json > previous.json
cx results show --scan-id <new-scan-id> --format json > current.json
diff <(jq -r '.results[].similarityId' previous.json | sort) \
<(jq -r '.results[].similarityId' current.json | sort)
# Step 4: temporarily pin the previous engine version while triaging, rather than blocking every deploy
cx scan create --project-name payments-api --source . --engine-version 2.34.1Interview Tip
A junior engineer typically panics and either bulk-approves everything to unblock the pipeline or treats every new finding as equally urgent, but for a senior role the interviewer is actually looking for a disciplined first question — did the code change or did the ruleset change — since a spike immediately following an engine update with minimal code diff points strongly at new detection rules, not new vulnerabilities. The strongest answers insist on spot-checking a sample of the new findings manually before bulk-dismissing anything, because a real regression can be hiding inside what's mostly ruleset noise, and blanket suppression under deploy pressure is exactly how a genuine new vulnerability slips through unnoticed. Mentioning a staged, report-only rollout process for engine upgrades shows mature AppSec program design, not just reactive incident triage.
◈ Architecture Diagram
engine v2.34 ──▶ v2.35 upgrade (new/changed queries)
│
↓
code unchanged, findings spike ──▶ likely ruleset noise
│
↓
spot-check sample ──▶ some real, most noise ──▶ triage both separately💬 Comments
Quick Answer
Track mean time to remediation for confirmed findings, the ratio of findings marked 'fixed' versus 'not exploitable'/'false positive' over time, and suppression rate per team or repository, since a team whose findings mostly disappear through suppression rather than actual code fixes is accumulating real risk while their dashboards look clean. The most telling single signal is a rising suppression-to-fix ratio trending upward over successive scans on the same codebase — that's a much stronger signal of a growing gap between reported and actual security posture than any single scan's raw finding count.
Detailed Answer
This is like a hospital tracking whether patients are actually getting treated versus just being discharged with their symptoms marked 'resolved' on paper without ever receiving care — a dashboard that only counts open-case numbers going down looks identical whether patients are recovering or whether staff are simply closing charts to hit a target. An AppSec program has exactly this same blind spot if it only measures the count of open findings trending downward, without distinguishing whether that decline came from developers actually patching vulnerable code or from findings being marked 'false positive' or 'risk accepted' by whoever's under the most deadline pressure that sprint.
This distinction matters because the two paths — genuine remediation and suppression — produce identically improving numbers on a naive open-findings dashboard while representing completely opposite security outcomes, and the incentive structure around a hard CI/CD policy gate can quietly push teams toward the easier path. If a pipeline blocks merges on any open high-severity finding, and a developer under deadline pressure has two options — spend hours actually fixing the underlying code, or click 'mark as not exploitable' and unblock the merge in thirty seconds — a purely count-based metric can't tell you which option is actually being chosen at scale across an organization, even though the security posture implications are entirely different.
Step by step, the metrics that actually separate these two outcomes: first, track the disposition breakdown of closed findings specifically — what fraction were closed because the underlying code was actually changed (verifiable, since a genuine fix means the finding disappears on a *rescan* of changed code, not just a manual status flip) versus closed via a manual triage action like 'not exploitable' or 'risk accepted' with no corresponding code change. Second, track mean time to remediation specifically for findings that were closed via actual code fixes, separate from the time-to-close for findings resolved via suppression, since blending these two very different processes into one aggregate 'time to resolve' number hides whether real fixes are happening promptly or slowly. Third, track suppression rate per team or repository as its own explicit metric, and specifically its trend over time — a team whose suppression rate is stable and low is likely triaging genuinely non-exploitable findings correctly, while a team whose suppression rate is climbing sharply is very plausibly using suppression as a pressure-relief valve against the CI gate rather than as a considered security judgment.
At production scale, mature AppSec teams pair this metric tracking with a lightweight audit process — periodically sampling a percentage of findings marked 'not exploitable' or 'risk accepted' for independent re-review by someone other than the developer who made that original call, specifically because self-triage under deadline pressure has an obvious, well-understood incentive problem, and a random-sample audit is far cheaper than reviewing every single suppression while still catching a team whose suppression judgment has drifted from genuine security assessment toward pipeline-unblocking convenience.
The gotcha: a team's suppression rate can look completely reasonable in isolation — say, 15% of findings marked not-exploitable, which sounds plausible for a mature codebase with genuinely low false-positive noise — while still representing a serious, hidden problem if that 15% disproportionately includes the small number of findings that were actually the most severe and exploitable ones, since raw suppression *rate* alone doesn't tell you anything about the *severity distribution* of what's being suppressed. An AppSec program that tracks suppression rate as a flat aggregate percentage without breaking it down by severity can completely miss a team that's diligently fixing every minor finding while consistently suppressing the rare, genuinely critical ones — the overall number looks healthy while the actual residual risk profile is inverted from what the dashboard implies.
Code Example
# Pull finding disposition breakdown for a project over the last quarter
cx results show --project-name payments-api --since 2026-04-01 --format json | \
jq '[.results[] | {state, severity}] | group_by(.state) | map({state: .[0].state, count: length})'
# Mean time to remediation, split by disposition type (fixed via code vs suppressed)
cx results show --project-name payments-api --filter "state=FIXED" --format json | \
jq '[.results[] | (.closedDate | fromdateiso8601) - (.firstFoundDate | fromdateiso8601)] | add/length'
# Suppression rate broken down BY SEVERITY, not just as one flat aggregate number
cx results show --project-name payments-api --filter "state=NOT_EXPLOITABLE,RISK_ACCEPTED" --format json | \
jq 'group_by(.severity) | map({severity: .[0].severity, count: length})'
# Flag a random 10% audit sample of suppressed critical/high findings for independent review
cx results show --filter "state=NOT_EXPLOITABLE" --filter "severity=CRITICAL,HIGH" --format json | \
jq '[.results[]] | .[range(0; length; 10)]'Interview Tip
A junior engineer typically proposes tracking 'number of open findings over time,' but for a senior role the interviewer is actually looking for the recognition that a declining open-findings count is ambiguous by itself — it looks identical whether developers are genuinely fixing code or just suppressing findings to unblock a CI gate under deadline pressure. The strongest answers insist on breaking suppression rate down by severity rather than as a flat aggregate, since a team can look healthy on an overall suppression percentage while specifically suppressing the small number of truly critical findings, inverting the actual risk picture the dashboard implies. Mentioning an independent audit-sampling process for self-triaged suppressions shows awareness of the incentive problem inherent in letting the same person who's under deadline pressure also be the sole judge of what's 'not exploitable.'
◈ Architecture Diagram
open findings trend: ●●●●●●● ── looks great either way breakdown: fixed via code: ●●●●●●● ✓ real improvement suppressed/accepted: ●●●●●●● or ●●●●●●● ✗ hidden risk
💬 Comments
Quick Answer
Baseline the scan against the target branch before the gate goes live, and configure the policy to fail only on findings introduced since that baseline — comparing each new scan's results by similarity ID against the baseline rather than gating on the total open-finding count. This lets a team adopt a hard security gate immediately without a multi-month backlog-clearing project blocking every merge from day one, while still guaranteeing no new critical risk gets introduced going forward.
Detailed Answer
Introducing a strict security gate into an established codebase is like a city suddenly enforcing a new building code retroactively on every existing structure — if every building built before the new code has to be brought into full compliance before anyone can pull a permit for anything, construction across the entire city grinds to a halt on day one, even for perfectly reasonable new projects that have nothing to do with the old buildings' problems. The sensible approach cities actually take is grandfathering: the new code applies in full to new construction and any building undergoing major renovation, while existing structures are handled through a separate, planned remediation timeline rather than an instant, blanket freeze. A Checkmarx policy gate needs exactly this same grandfathering design, or it becomes an organizational non-starter the moment someone realizes turning it on immediately blocks every single merge across a codebase that's accumulated years of pre-existing findings.
This baseline-then-gate-on-new-only approach exists because the alternative — gating on total open finding count from day one — creates a perverse, predictable outcome: teams facing an instantly-blocked pipeline either revolt against the security gate entirely and get it disabled, or they burn enormous effort on an emergency backlog-clearing sprint that displaces actual planned work, or worst of all, they mass-suppress the entire existing backlog just to get the gate to pass, which defeats the purpose of having accurate finding data at all. None of these outcomes are what the security team actually wanted when they introduced the gate — the goal was preventing new risk from entering the codebase, not an ultimatum to fix years of debt overnight.
Step by step, implementing this correctly: first, run a baseline scan against the current state of the target branch (main) before the gate becomes enforcing, and treat every finding present at that baseline scan as accepted, pre-existing technical debt — tracked and visible, but explicitly not blocking. Second, configure subsequent scans (on pull requests or merge commits) to diff their findings against that baseline using each finding's similarity ID (a stable identifier Checkmarx computes based on the code location and pattern, which persists across scans even as line numbers shift from unrelated code changes elsewhere in the file) rather than comparing raw finding counts, since raw counts can't distinguish 'a pre-existing finding that just moved because of an unrelated edit two lines above it' from 'a genuinely new finding introduced by this specific change.' Third, the actual policy gate logic checks only the delta — any similarity ID present in the new scan but absent from the baseline is treated as newly introduced, and it's this delta set, not the total open-finding count, that the merge-blocking threshold evaluates against.
At production scale, this pattern also needs an explicit plan for the pre-existing backlog, or grandfathering silently becomes permanent amnesty — the baseline findings should still appear on a tracked, visible backlog with their own remediation targets and periodic review cadence, just decoupled from the immediate, hard-blocking merge gate, so the team gets both outcomes: a strict, enforced gate against new risk starting immediately, and a realistic, non-disruptive path to gradually working down the existing debt without an artificial fire-drill deadline forcing rushed, corner-cutting fixes.
The gotcha: similarity-ID matching, while far better than raw counts, isn't perfectly stable across every kind of code change — a sufficiently large refactor (renaming a function, restructuring a class, moving code between files) can change a finding's computed similarity ID even though the underlying vulnerable pattern itself didn't meaningfully change, which makes an old, pre-existing finding look 'new' to the diff and unexpectedly triggers the gate on a change that didn't actually introduce new risk. Teams that don't anticipate this occasionally hit a confusing false gate-failure right after a large but security-neutral refactor, and the fix isn't disabling the gate, it's recognizing the specific finding as a baseline-carryover during triage and explicitly re-associating it with the original baseline entry rather than treating it as newly introduced.
Code Example
#!/bin/bash set -euo pipefail # One-time baseline scan against main BEFORE the gate becomes enforcing cx scan create --project-name payments-api --source . --branch main --tag baseline BASELINE_SCAN_ID=$(cx scan list --project-name payments-api --tag baseline --format json | jq -r '.[0].id') # Per-PR pipeline step: scan the PR branch and diff against the baseline by similarity ID cx scan create --project-name payments-api --source . --branch "$CI_COMMIT_BRANCH" --tag pr-scan PR_SCAN_ID=$(cx scan list --project-name payments-api --tag pr-scan --format json | jq -r '.[0].id') cx results show --scan-id "$BASELINE_SCAN_ID" --format json | jq -r '.results[].similarityId' | sort > baseline_ids.txt cx results show --scan-id "$PR_SCAN_ID" --filter "severity=CRITICAL,HIGH" --format json | \ jq -r '.results[].similarityId' | sort > pr_ids.txt # Gate only on findings NEW to this PR, not the full pre-existing backlog NEW_FINDINGS=$(comm -13 baseline_ids.txt pr_ids.txt | wc -l) if [ "$NEW_FINDINGS" -gt 0 ]; then echo "BLOCKED: $NEW_FINDINGS new critical/high findings introduced by this change" exit 1 fi echo "PASS: no new critical/high findings vs baseline"
Interview Tip
A junior engineer typically proposes 'fail the build if there are any high or critical findings,' but for an architect-level role the interviewer is actually looking for the baseline-and-diff design, because gating on total open findings in an established codebase immediately blocks every merge and forces either a revolt against the gate or a mass-suppression event that destroys the accuracy of the finding data. The strongest answers explain similarity-ID-based diffing specifically, and proactively flag the edge case where a large but security-neutral refactor can shift a pre-existing finding's similarity ID enough to look like a new finding, unexpectedly tripping the gate. Mentioning that grandfathered backlog findings still need a visible, tracked remediation plan — not silent permanent amnesty — shows understanding that baselining is a rollout strategy, not a way to make old risk disappear from consideration.
◈ Architecture Diagram
baseline scan (main) ──▶ 340 pre-existing findings (tracked, not gating)
│
PR scan ──▶ 342 findings total
│
diff by similarity ID ──▶ 2 NEW findings
│
gate checks ONLY the 2 new ──▶ ✗ block merge / ✓ pass💬 Comments
Quick Answer
SAST (Static Application Security Testing) scans source code without running it, DAST (Dynamic Application Security Testing) attacks a running application from the outside, and SCA (Software Composition Analysis) checks your dependencies against known vulnerability databases — each sees a different slice of risk and each has different blind spots. Checkmarx One correlates findings across all three, so a SAST-flagged code path that actually calls a vulnerable library (caught by SCA) and is genuinely reachable from an exposed endpoint (confirmed by DAST) gets treated as high-confidence and exploitable, while an isolated SAST finding with no corroborating evidence gets deprioritized as a likely false positive.
Detailed Answer
Think of three different building inspectors checking the same house for safety, each using a completely different method. One inspector reads the blueprints without ever stepping inside — they can spot a structural flaw in the design itself, like a load-bearing wall drawn incorrectly, but they'll also flag things that look risky on paper but were actually built correctly and reinforced on-site, something the blueprints alone don't show. A second inspector actually walks through the finished house, opening doors, testing the wiring, checking if anything currently misbehaves — they catch real, live problems, but only in the rooms they actually visit, and won't catch a structural flaw sitting in a wall nobody happened to open that day. A third inspector doesn't look at the house at all — they just check whether any of the prefabricated materials used (specific window brands, electrical panels) are on a public recall list. SAST is the blueprint reader, DAST is the walkthrough inspector, and SCA is the recall-list checker, and a serious safety assessment needs all three because each one catches things categorically invisible to the other two.
Checkmarx (and application security tooling generally) evolved to combine all three specifically because each technique in isolation produces a predictable, well-known failure mode. SAST, because it analyzes code without ever executing it, tends toward high recall but also high false positives — it flags a huge number of *theoretically* dangerous code patterns (like unsanitized input flowing into a SQL query) without knowing whether that pattern is actually reachable by an attacker or whether some other layer of the application already sanitizes the input elsewhere. DAST, because it only tests what it can actually reach and exercise at runtime, has excellent precision (a finding it reports is almost certainly real and exploitable) but poor coverage, since it can only find issues along code paths its scan actually triggers, missing anything behind authentication it wasn't configured to bypass or rare execution branches it never happens to hit. SCA solves an entirely different problem — it doesn't analyze your code's logic at all, it just matches your dependency manifest against CVE (Common Vulnerabilities and Exposures) databases, so it's extremely good at telling you *that* you're using a vulnerable library version, but says nothing about whether your code actually calls the vulnerable function inside that library in an exploitable way.
Step by step, Checkmarx's correlation engine works by building a unified model across all three data sources rather than treating them as three separate reports a human has to manually cross-reference. It takes SAST's data-flow analysis (which traces how untrusted input moves through the code — for example, from an HTTP request parameter through several function calls into a database query) and checks whether that flow passes through a function that SCA has already flagged as belonging to a vulnerable dependency version. It further checks whether DAST's runtime crawl actually reached and exercised that same code path during a live scan. A finding that lines up across all three signals — a data-flow path exists (SAST), it runs through vulnerable dependency code (SCA), and it's confirmed reachable at runtime (DAST) — gets surfaced as high-confidence and exploitable. A SAST finding with no corroborating SCA or DAST signal doesn't get discarded outright, but it gets deprioritized in the triage queue, which is the actual mechanism by which correlation reduces the false-positive burden on security teams without silently dropping potentially real findings.
At production scale, this correlation matters enormously for how AppSec teams actually spend their time: without it, a single codebase easily produces thousands of raw SAST findings, the overwhelming majority of which are either not reachable in practice or already mitigated by controls the static analyzer couldn't see (like a shared input-sanitization middleware applied globally). Security teams that must manually triage that volume burn out fast and start rubber-stamping findings as false positives just to keep pace, which is precisely how a genuinely exploitable vulnerability slips through — buried in thousands of look-alike low-confidence findings. Correlation-based prioritization is what makes SAST results operationally usable at real codebase scale rather than a wall of noise developers learn to ignore.
The gotcha: a finding that correlation marks as *lower* confidence is not the same as a finding that's actually safe to ignore — a data-flow path SAST identifies but DAST never happened to exercise during its crawl (because the DAST scan's configured user role never navigated to that specific page, for instance) will show reduced correlated confidence purely due to a coverage gap in the dynamic scan, not because the underlying code is actually safe. Teams that treat 'lower correlated confidence' as synonymous with 'not a real risk' rather than 'not yet corroborated' can end up deprioritizing a genuinely exploitable finding simply because their DAST scan's crawl configuration never reached that part of the application.
Code Example
# Trigger a Checkmarx One scan combining SAST and SCA for payments-api cx scan create --project-name payments-api \ --source . \ --branch main \ --scan-types sast,sca \ --sast-preset "Checkmarx Default" # Add a policy gate: fail the pipeline on new high-severity, hard fail on any critical cx scan create --project-name payments-api \ --source . \ --branch main \ --scan-types sast,sca \ --threshold "critical=0;high=0;medium=10" # Pull correlated results specifically (findings confirmed across multiple engines) cx results show --scan-id <scan-id> --filter "state=CONFIRMED" # Query SCA findings only, to see which flagged libraries are actually reachable in code cx results show --scan-id <scan-id> --scan-types sca --filter "reachability=REACHABLE"
Interview Tip
A junior engineer typically lists the three acronyms and their generic definitions, but for a senior role the interviewer is actually looking for whether you understand the specific failure mode each technique has in isolation — SAST's high false-positive rate from lacking runtime context, DAST's coverage gaps from only testing what it happens to crawl, and SCA's blindness to whether vulnerable code is actually called — and how correlating all three specifically targets those individual weaknesses. The strongest answers warn that a lower correlated-confidence score means 'not yet corroborated,' not 'safe to ignore,' since a real vulnerability sitting on a code path the DAST scan's crawl never reached will show artificially low confidence purely from a scan coverage gap, not from the underlying risk being smaller.
◈ Architecture Diagram
SAST: data-flow exists ──┐ SCA: vulnerable dep? ─────│──▶ correlated ──▶ ✓ high confidence DAST: reachable live? ────┘ only SAST flags it, no corroboration ──▶ lower priority (still worth checking)
💬 Comments
Context
An AppSec team whose Checkmarx rollout was drowning: the default preset flagged ~12,000 findings across 60 apps, developers triaged by ignoring emails, and the two security engineers spent their weeks re-marking the same false positives after every full rescan.
Problem
Default presets fire on every theoretically-tainted flow; framework-managed sanitization (ORM parameterization, template auto-escaping) generated thousands of not-exploitable results; triage state occasionally reset with project reconfigurations; and 'developers actually fix Checkmarx findings' was zero for three quarters.
Solution
Ran a tuning program: built a custom preset from the top 25 queries that map to the org's actual risk (injection, authn/z, secrets, deserialization) instead of the everything preset; customized queries to recognize the org's standard sanitizers and ORM patterns (CxQL overrides registering framework methods as sanitizers); established triage governance — Not Exploitable requires justification text and second-review for criticals, with state audited quarterly; and moved scanning to incremental-on-PR with weekly full scans, so developers see only findings their change introduced.
Commands
preset: top-25 queries by org risk; disable info/low noise queries
CxQL override: add SqlSanitize -> org.internal.db.SafeQuery.* patterns
PR flow: incremental scan -> new findings only -> PR comment with data-flow trace
Outcome
Open findings dropped from ~12,000 to ~3,500 real candidates (70% noise removal); developer fix rate went from ~0 to 60% of new findings within sprint; the security engineers stopped re-triaging and started query engineering. The quarterly Not-Exploitable audit caught two wrong dismissals — the process works both directions.
Lessons Learned
Sanitizer registration was the highest-leverage tuning — the tool wasn't wrong that data flowed; it didn't know the org's escaping layer. New-findings-only PR feedback is what made developers engage; the historical pile is a security-team program, not a developer inbox.
💬 Comments
Context
A B2B software vendor whose largest enterprise customer added a contractual requirement: every release must carry evidence of SAST scanning with zero unmitigated criticals, auditable per version.
Problem
Scans ran 'regularly' but weren't tied to releases — proving which code state was scanned for which shipped version was reconstruction work; findings lacked disposition trails; and the release process had no security gate at all, just a dashboard nobody was contractually reading.
Solution
Bound scanning to the release pipeline: release candidates trigger a full Checkmarx scan pinned to the release SHA; the policy gate (zero criticals, zero highs without approved mitigation) blocks the release artifact from promotion; every finding on the release branch carries a disposition (fixed, mitigated-with-justification, not-exploitable-with-review) exported via the Checkmarx API into a signed release-evidence bundle (scan ID, SHA, preset version, findings + dispositions, approver identities) attached to the release. The customer's auditors get the bundle; engineering gets a deterministic gate.
Commands
release CI: cx scan create --project app --branch release/$VER --sha $SHA --preset org-standard-v3
gate: cx results --scan-id $ID --policy 'critical=0,high:mitigated-only' || block
evidence: cx results export -> sign -> attach to release artifacts
Outcome
The attestation requirement is met mechanically per release (auditor spot-checks passed twice); release-blocking findings get fixed pre-release instead of dashboard-aging; and the evidence bundle turned a contractual risk into a sales asset cited in two later deals.
Lessons Learned
Pinning the preset version in evidence matters — an auditor asked whether the ruleset changed between releases, and the answer was in the bundle. The gate forced the mitigation-justification discipline that dashboards never had.
💬 Comments
Symptom
After an infrastructure migration of the Checkmarx server, the next full scans return findings with all historical triage state gone — thousands of previously Not-Exploitable findings reappear as New. Teams bulk-dismiss the flood to get pipelines green. Three months later, a pentest finds an exploitable injection that Checkmarx had flagged — and someone had bulk-dismissed.
Error Message
No error. Scan results show result states reset; the migration had recreated projects (new project IDs) instead of restoring them, and result-state history is keyed to the project — new project, blank history.
Root Cause
The migration treated Checkmarx projects as recreatable configuration rather than stateful assets: triage state (Not Exploitable markings, comments, assignments) lives with the project/scan lineage, and recreating projects orphaned it. The flood of resurrected findings then incentivized exactly the wrong behavior — bulk dismissal under pipeline pressure — which laundered one genuine critical into the noise.
Diagnosis Steps
Solution
Restored what could be restored (the old database was recoverable; state migrated properly the second time), then audited every bulk dismissal from the flood window against the restored history — the pentest finding plus two more real issues had been swept. Process fixes: triage state included explicitly in the platform's backup/restore runbook and DR tests; bulk state changes require security-team approval; and result-state export runs weekly as an independent backup.
Commands
cx api: GET /projects — compare IDs pre/post migration
audit: results where state changed to NotExploitable during flood window, grouped by actor
weekly: export result states -> versioned backup
Prevention
Triage state is data, not config — back it up, test its restore, and treat any mass state-reset as an incident (stop pipelines, restore, investigate) rather than a triage-harder event. Rate-limit and gate bulk dismissals; a thousand findings dismissed in an afternoon is a signal, not a cleanup.
💬 Comments
Symptom
After consolidating four repos into a monorepo, Checkmarx full scans that previously took 40 minutes each now run 8+ hours and frequently fail on engine limits; the release-gate scan can't complete inside the pipeline's window, and releases queue behind a scanner.
Error Message
Scan status: Failed — 'Engine reached the maximum scan time'; earlier warnings: 'Project size exceeds recommended LOC for single-engine scan' and memory ceiling logs on the engine host.
Root Cause
The monorepo import multiplied effective scan scope: the SAST engine's data-flow analysis scales super-linearly with codebase size and cross-file flow candidates, and the project's scan scope included every service plus vendored third-party code and generated files (protobuf output, minified assets) — tripling LOC with zero security value. One project scanning everything serially replaced four bounded parallel scans.
Diagnosis Steps
Solution
Re-scoped scanning to match the architecture: per-service Checkmarx projects with path-based scan scopes inside the monorepo (each service scans its own tree plus shared libs), exclusion filters for vendored/generated code (a third of the LOC), incremental scans on PR paths with full scans nightly per service in parallel. The release gate now aggregates per-service scan results rather than one monolithic scan. Total wall-clock for full coverage: 50 minutes in parallel.
Commands
scan config: include services/payments/**, shared/lib/**; exclude **/vendor/**, **/*_pb2.py, **/dist/**
nightly matrix: per-service full scans in parallel
metric: scan_duration by project — alert on trend
Prevention
Scan scope is an architectural decision: SAST projects should map to deployable units, not VCS repositories. Always exclude generated/vendored code explicitly (measure LOC before and after). Watch scan duration as a trend metric — super-linear growth warns before the timeout wall.
💬 Comments