Security testing does not need to begin after an application is running.
A large class of security problems can be detected while the code is still sitting in a repository, before a developer deploys it, before a pentester reaches the application, and ideally before the vulnerable change is merged at all.
That is where Semgrep Community Edition fits.
Semgrep is an open-source static analysis tool that analyzes source code for insecure patterns, bugs, and coding-policy violations. Its Community Edition includes the SAST engine, community rules, and editor integrations, and it can be executed locally or inside CI/CD pipelines.
This guide focuses on Semgrep CE. You will learn where it fits in an AppSec workflow, how its rules work, how to scan a repository, how to interpret findings, how to control the scan scope, how to export results, how to integrate it into CI/CD, and finally how to create a simple custom rule.
What Is Semgrep?
Semgrep is a Static Application Security Testing, or SAST, tool.
Unlike DAST tools such as OWASP ZAP, which interact with a running application, SAST tools analyze the application’s source code.
The simplified workflow looks like this:
Source Code -> Static Analysis -> Security Findings -> Developer Review / Fix
Semgrep uses rules to describe code structures that should be detected.
Those rules can identify:
- potentially insecure functions;
- dangerous API usage;
- coding-policy violations;
- bug patterns;
- insecure configuration;
- organization-specific code that should require review.
Semgrep rules can use both pattern matching and data-flow analysis. Community rules can be obtained through the Semgrep Registry, while organizations can also maintain their own rules.
The important distinction is that Semgrep understands code structure, rather than simply searching source files as plain text.
Where Semgrep Fits in the Security Workflow
Semgrep can be useful at several points in a Secure Software Development Lifecycle.
One developer may run it locally before committing code:
Developer -> Semgrep -> Commit
A development team can place it in the pull request workflow:

An AppSec team can also use Semgrep during code reviews or security assessments.
That gives Semgrep a different role from tools such as Burp Suite or OWASP ZAP.
Burp and ZAP help answer:
What security problems can I observe while interacting with the application?
Semgrep helps answer:
What suspicious or insecure patterns exist in the code that builds the application?
Neither replaces the other.
They observe the application from different perspectives.
When and Why to Use It
Semgrep becomes particularly useful when security checks need to be repeatable.
A pentester may notice a vulnerable pattern once during a code review. Semgrep allows that knowledge to become a rule that can continuously search for the same pattern.
For example, imagine a team decides that direct calls to a dangerous function should never appear inside production code.
Instead of relying on someone remembering to check every pull request manually, the organization can create a Semgrep rule and execute it automatically.
This makes the tool useful for:
- SAST during application security assessments;
- automated security checks in CI/CD;
- secure coding enforcement;
- code review assistance;
- detecting variants of previously discovered bugs;
- verifying that insecure functions or patterns are not reintroduced.
The scanner finds candidates for investigation.
A Semgrep finding does not automatically prove that a vulnerability is exploitable. Context still matters, especially when the rule intentionally searches broadly.
That distinction becomes important when reviewing results.
How Semgrep Thinks About Code
Imagine the following Python code:
import subprocesssubprocess.run( ["ping", "-c", "1", "127.0.0.1"])
A plain text search can look for the string:
subprocess.run
Semgrep can describe the structure of the call instead.
For example:
subprocess.run(..., shell=True, ...)
Here, ... means that other arguments may exist around the part we care about.
Semgrep rules can also use metavariables.
A metavariable begins with $ and represents a piece of code that Semgrep should capture.
Conceptually:
subprocess.run($COMMAND, ...)
$COMMAND can represent different expressions passed into that position.
This allows a rule to describe a family of code structures rather than one exact line of text.
More complex rules can combine patterns, exclusions, metavariables, and data-flow analysis, but understanding these two ideas is enough to start:
... → some code may exist here$VARIABLE → capture part of the matched code
The official Semgrep rule documentation expands these concepts into more advanced rule logic.
Installation
Semgrep CE can be installed through pipx or uv. On macOS, Homebrew is also supported, although Semgrep notes that the Homebrew package can lag behind the latest release.
Using pipx:
pipx install semgrep
Using uv:
uv tool install semgrep
After installation:
semgrep --version
You should receive the installed Semgrep version.
Your First Scan
Create a directory for the test:
mkdir semgrep-labcd semgrep-lab
Now create app.py:
import osdirectory = input("Directory: ")os.system("ls " + directory)
The problem is easy to identify manually.
User-controlled data is concatenated into a string that is interpreted by a shell.
Instead of reviewing it manually, run Semgrep:
semgrep scan --config auto .
The auto configuration selects rules according to the languages and frameworks detected in the project. Semgrep can also receive one or more specific rulesets through --config.
For this example, we can narrow the scan to Python command injection rules:
semgrep scan --config "p/python-command-injection" app.py
This ruleset focuses specifically on Python command injection patterns.
Semgrep should report the call to:
os.system("ls " + directory)
The exact output may vary as community rules evolve, but the important part is the finding itself: Semgrep identifies the file, the affected code, the rule that generated the finding, and an explanation of why the pattern may be dangerous.
Reading a Finding
A scanner result should be treated as the beginning of an investigation, not the end of one.
In our example, the relevant flow is:
input() -> directory -> string concatenation -> os.system()
The value comes from the user and eventually becomes part of a shell command.
That makes the finding particularly interesting because the issue is not simply that os.system() exists.
The important question is:
Can attacker-controlled data reach a security-sensitive operation?
This source-to-sink relationship is a common concept in static analysis.
A source is where potentially untrusted data enters the application.
A sink is an operation where that data could become dangerous, such as command execution, SQL queries, file access, or HTML rendering.
Some Semgrep rules only search for specific code structures. Others use taint analysis to track data as it moves between sources and sinks.
This is also why findings require human review. The scanner can identify suspicious flows, but the developer or security analyst still needs to understand whether the surrounding application makes the condition reachable and security-relevant.
Controlling the Scan Scope
Running Semgrep against an entire repository is simple:
semgrep scan --config auto .
But real repositories often contain generated files, dependencies, test fixtures, or directories that do not need to be analyzed.
You can exclude paths directly:
semgrep scan --config auto --exclude tests/ --exclude vendor/ .
Or restrict the scan to a specific area:
semgrep scan --config auto --include src/ .
The --include and --exclude options use path patterns to decide which files should become scan targets. Multiple exclusions or inclusions can be provided when necessary. Exclusions are processed before inclusions, so an included path does not bring back something that was already excluded.
For exclusions that should remain part of the project configuration, use a .semgrepignore file instead of repeating command-line arguments.
For example:
vendor/,dist/,tests/fixtures/
Semgrep automatically considers .semgrepignore when selecting scan targets.
This distinction is useful in practice:
--include / --exclude -> Temporary scan scope -> .semgrepignore -> Repository-level scan policy
Keeping the scan scope intentional reduces unnecessary findings and makes scan results easier to review.
Understanding Severity
Every Semgrep rule has a severity.
Current Semgrep rules can use:
LOW, MEDIUM, HIGH, CRITICAL,
The older INFO, WARNING, and ERROR levels remain supported for backwards compatibility.
Severity is defined by the rule author and represents how important a finding is expected to be, not proof that the affected code is exploitable.
A high-severity result still needs context.
For example:
Severity -> Reachability? -> Attacker-controlled input? -> Existing protections? -> Actual risk
This is particularly important when using large community rulesets.
Severity helps prioritize review, but it should not replace analysis.
Exporting Semgrep Results
Terminal output works well while investigating a repository manually.
Automation usually needs structured output.
Semgrep supports JSON output:
semgrep scan --config auto --json-output semgrep-results.json .
The generated file includes information such as the rule identifier, affected path, location, message, severity, and rule metadata.
You can also export SARIF:
semgrep scan --config auto --sarif-output semgrep-results.sarif .
SARIF, or Static Analysis Results Interchange Format, is designed for exchanging static-analysis results between tools.
Platforms such as GitHub Code Scanning can ingest SARIF produced by third-party scanners and expose the findings inside the repository security interface.
This makes structured output useful when Semgrep needs to feed:

Using Semgrep in CI/CD
Running Semgrep manually is useful during an assessment.
Running it automatically is where static analysis becomes a continuous security control.
For a standalone Community Edition deployment, Semgrep recommends using semgrep scan.
The same command used locally can therefore become another step in a CI pipeline.
A minimal GitHub Actions workflow could look like this:
name: Semgrep CEon: pull_request: push: branches: - mainpermissions: contents: readjobs: semgrep: runs-on: ubuntu-latest container: image: semgrep/semgrep steps: - uses: actions/checkout@v6 - name: Run Semgrep run: semgrep scan --config auto --error .
The official Semgrep CE examples use the semgrep/semgrep container and run semgrep scan inside the CI job.
There is one important difference between simply scanning and using the scan as a security gate.
By default:
semgrep scan --config auto .
reports findings but does not fail only because findings were detected.
Adding:
--error
causes semgrep scan to return exit code 1 when findings exist.
This allows the CI system to treat the result as a failed job.
That does not mean every Semgrep rule should immediately block every pull request.
A practical rollout may look like:
New rule -> Monitor findings -> Validate false positives -> Tune rule -> Enable CI enforcement
Blocking developers with a noisy rule usually creates a second problem instead of solving the first one.
[IMAGE 5 — GitHub Actions pull request failing because Semgrep detected a finding]
Writing Your First Custom Rule
Community rules cover many common vulnerability classes, but one of Semgrep’s most useful characteristics is that rules can describe patterns specific to your own codebase.
Suppose your team decides that os.system() should always require manual review.
Create:
rules.yml
with:
rules: - id: python-os-system message: Review command execution through os.system(). severity: HIGH languages: - python pattern: os.system(...)
A basic Semgrep rule contains an identifier, message, severity, language, and matching logic.
Rules can later grow to use combinations of patterns, exclusions, metavariables, regex conditions, or data-flow analysis.
Run the custom rule:
semgrep scan --config rules.yml app.py
Any matching call such as:
os.system("date")
will now generate a finding.
Notice what this rule does not say.
It does not claim that every os.system() call is vulnerable.
It says:
We care about this code pattern.When it appears, show it to us.
That is already useful.
We can also capture the argument using a metavariable:
rules: - id: python-os-system message: Review command execution through os.system(). severity: HIGH languages: - python pattern: os.system($COMMAND)
$COMMAND now represents whatever expression is passed into the function.
From there, rules can become increasingly precise.
For example, instead of detecting every dangerous function call, a more advanced rule may attempt to identify:
Untrusted source -> Data transformations -> Dangerous sink
That progression is important.
The goal of custom rule development is usually not to write the most complicated rule possible.
It is to describe the security property you actually want to enforce with enough precision that developers can trust the result.
The official Semgrep rule syntax documentation covers operators such as patterns, pattern-either, pattern-not, pattern-inside, metavariable constraints, and other mechanisms for refining rules.
[IMAGE 6 — Custom Semgrep rule detecting os.system() in app.py]
Turning AppSec Knowledge Into Rules
This is where Semgrep becomes especially interesting from an AppSec perspective.
Imagine that during a pentest or code review you discover the following recurring pattern:
User input -> Internal helper -> Dangerous operation
Fixing one instance solves one vulnerability.
Understanding the pattern gives you something more valuable.
You can investigate whether the same mistake exists elsewhere:
semgrep scan --config custom-rule.yml .
If the rule proves reliable, it can then move into CI/CD:
Security finding -> Understand root pattern -> Create Semgrep rule -> Search existing repositories -> Add rule to CI/CD -> Prevent reintroduction
This is sometimes called bug-variant analysis: instead of treating a discovered vulnerability as an isolated event, use its underlying code pattern to search for related implementations.
The same idea applies beyond vulnerabilities.
Custom rules can enforce internal security decisions.
Semgrep rules are therefore not limited to generic vulnerability detection.
They can also encode knowledge that only exists inside a particular development or security team.
Semgrep’s rule system explicitly supports custom checks for secure-coding violations, code review automation, and configuration scanning.
The AppSec Mindset
The most productive way to use Semgrep is not:
Run scanner -> Receive 200 findings -> Declare 200 vulnerabilities
Instead:
Choose relevant rules -> Control scan scope -> Review findings -> Understand source and sink -> Validate application context -> Tune noisy rules -> Automate reliable checks
Start with existing rules from the Semgrep Registry.
Inspect what they actually detect.
When a finding appears, read the affected code instead of relying only on its title or severity.
When you discover a security pattern that matters to your environment, try expressing it as a custom rule.
When that rule becomes reliable enough, move it into CI/CD.
At that point, Semgrep stops being only a scanner you run during a pentest.
It becomes a way to turn AppSec knowledge into repeatable checks that follow the code every time it changes.

