A static code scan analyzes your source code without executing it, tracing how data flows through the program to flag injection, hardcoded secrets, unsafe deserialization, and other defects before the code ever runs. This is the discipline the industry calls SAST (static application security testing), and its defining trait is that it reasons about all the paths through your code, including the error branches your tests never exercised. That breadth is its superpower and the source of its most common frustration: false positives.
I run a static code scan on every branch, and I treat its output the way I treat a smoke detector. Most alarms are burnt toast. The ones that are not will save the building.
How a static code scan works
The scanner parses your code into an abstract syntax tree, then builds a control-flow and data-flow graph on top of it. From there it performs taint analysis: it marks data entering from untrusted sources (an HTTP parameter, a file, an environment variable) as tainted, and follows that taint through assignments and function calls to see whether it reaches a dangerous sink (a SQL query, a shell command, an HTML response) without passing through a sanitizer.
When tainted data reaches a sink unsanitized, you get a finding. That is the whole game, expressed in one sentence, though the engineering to do it accurately across a real codebase is substantial.
# A taint-tracker flags this: request data reaches a SQL sink unsanitized
username = request.args.get("user") # source (tainted)
query = "SELECT * FROM users WHERE name = '" + username + "'" # sink
cursor.execute(query) # SQL injection
The fix the scanner wants to see is a parameterized query, where the value never becomes part of the SQL text:
cursor.execute("SELECT * FROM users WHERE name = %s", (username,))
What a static scan catches well
Certain bug classes are structurally visible in source, and this is where static analysis shines:
- Injection flaws: SQL, OS command, LDAP, and template injection, all of which follow the source-to-sink pattern.
- Hardcoded secrets: API keys, private keys, and passwords committed into the repo.
- Unsafe APIs: calls to weak crypto, insecure random, and deserialization of untrusted data.
- Error-path bugs: null dereferences and resource leaks on branches that integration tests skip.
Because the scan reads every line rather than every executed line, it reaches code that only runs on the third Tuesday of a leap year. No dynamic test gives you that coverage.
What it misses
A static code scan cannot see anything that only exists at runtime. It does not know the actual value of a config flag, whether an auth middleware is wired in front of a route, or that your reverse proxy strips a header. This is the domain of dynamic testing, which exercises the running application from the outside. The two are complementary rather than competing, and a mature program runs both; our DAST product page explains where dynamic analysis picks up what static analysis structurally cannot.
The other big miss is business logic. A scanner has no idea that a user should not be able to refund an order twice, because nothing in the syntax says so. Logic flaws need humans or tests that encode the rules.
Running a scan in CI without drowning in noise
The failure mode that kills adoption is a scan that reports 4,000 findings on its first run and blocks the pipeline. Engineers route around it within a week. Two practices prevent that.
First, baseline. Record the existing findings as a known set and only fail the build on new issues introduced by the current change. This makes the scan about not-getting-worse, which is a battle you can actually win.
# GitHub Actions: scan changed code, fail only on new high-severity findings
- name: Static analysis
run: |
semgrep ci --config auto --baseline-commit "$GITHUB_BASE_REF" \
--severity ERROR
Second, tune the ruleset. Ship with a small, high-signal rule pack, earn trust, then expand. A scan that developers believe is a scan they will actually read.
Triaging findings
Not every true positive is worth fixing today. Rank by exploitability and reachability. A tainted-to-sink flow on a public, unauthenticated endpoint outranks the same pattern in an internal admin script gated behind SSO. Reachability analysis, which asks whether a vulnerable function is actually called on a live path, cuts the queue dramatically. This matters even more for dependency findings, where a CVE in a package you import but never call is real on paper and irrelevant in practice.
When a finding does trace to a third-party component rather than your own code, that is composition analysis territory rather than pure SAST, and the two feed each other. If you are weighing tools, our comparison with Snyk covers how static scanning and dependency scanning are packaged.
Making it stick
The teams that get value from static scanning treat it as a habit, not an audit. Scan on every pull request, keep the run under a few minutes, fail only on new high-severity issues, and give developers fixes inline where they read code. A scan that takes twenty minutes and blocks on legacy debt is a scan that gets disabled. A scan that quietly catches the one SQL concatenation a junior engineer wrote on Friday afternoon is one that pays for itself.
FAQ
What is the difference between a static code scan and a dynamic scan?
A static code scan (SAST) analyzes source code without running it, so it can see every code path and pinpoint the exact line. A dynamic scan (DAST) tests the running application from the outside and catches runtime and configuration issues that source analysis cannot see. Mature teams run both.
Why does my static code scan report so many false positives?
Static analysis errs toward reporting a potential flaw rather than missing one, and it lacks runtime context such as config values or whether a sanitizer is actually reached. Reduce noise by baselining existing findings, tuning to a high-signal ruleset, and prioritizing by reachability.
Can a static code scan replace code review?
No. It excels at pattern-based flaws like injection and hardcoded secrets, but it cannot detect business-logic errors, since nothing in the syntax expresses the intended rules. Use it to free reviewers to focus on logic and design.
How long should a static scan take in CI?
For pull-request feedback, aim for under a few minutes by scanning only changed code against a baseline. Reserve full, deep scans for a nightly or pre-release job where a longer run time is acceptable.