Why SAST Matters in Your Pipeline
Static Application Security Testing analyzes source code without executing it. You catch SQL injection, cross-site scripting, hardcoded credentials, and buffer overflows before the code ever runs. The earlier you catch these, the cheaper they are to fix — a vulnerability found in development costs 6x less than one found in production.
This guide covers five languages with real tool configurations you can drop into your pipeline today. No theory-only explanations here — every section includes the install command, the run command, the CI step, and an example of what the scanner actually catches.
---
Java — SonarQube with Maven and Gradle
Why SonarQube for Java
SonarQube has the deepest rule set for Java — over 600 rules covering security, reliability, and maintainability. Its quality gate mechanism lets you block merges when security issues exist.
Installation and Setup
For local development, run SonarQube in Docker:
docker run -d --name sonarqube \
-p 9000:9000 \
-v sonarqube_data:/opt/sonarqube/data \
-v sonarqube_logs:/opt/sonarqube/logs \
sonarqube:lts-community
Access the dashboard at http://localhost:9000 (default credentials: admin/admin).
Generate a project token from Administration then Security then Users then Tokens.
Maven Integration
Add the SonarQube plugin to your pom.xml:
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.11.0.3922</version>
</plugin>
Run the analysis:
mvn clean verify sonar:sonar \
-Dsonar.projectKey=my-java-app \
-Dsonar.host.url=http://localhost:9000 \
-Dsonar.token=sqp_your_token_here
Gradle Integration
In build.gradle:
plugins {
id "org.sonarqube" version "5.0.0.4638"
}
sonar {
properties {
property "sonar.projectKey", "my-java-app"
property "sonar.host.url", "http://localhost:9000"
property "sonar.token", System.getenv("SONAR_TOKEN")
}
}
Run with:
./gradlew sonar
Quality Gate Configuration
In SonarQube, navigate to Quality Gates and create a custom gate:
- New Security Hotspots: 0 (no new security issues allowed)
- New Vulnerabilities: 0
- New Code Coverage: greater than 80 percent
- New Duplicated Lines: less than 3 percent
This means any PR introducing a security vulnerability will fail the quality gate.
GitHub Actions Step
- name: SonarQube Analysis
uses: sonarsource/sonarqube-scan-action@v2
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
with:
args: >
-Dsonar.projectKey=my-java-app
-Dsonar.java.binaries=target/classes
- name: SonarQube Quality Gate
uses: sonarsource/sonarqube-quality-gate-action@v1
timeout-minutes: 5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Sample Vulnerable Code
public User findUser(String username) {
String query = "SELECT * FROM users WHERE name = '" + username + "'";
return jdbcTemplate.queryForObject(query, new UserRowMapper());
}
SonarQube flags this as a Blocker vulnerability with rule java:S3649: "Change this code to not construct SQL queries directly from user-controlled data." The fix uses parameterized queries:
public User findUser(String username) {
String query = "SELECT * FROM users WHERE name = ?";
return jdbcTemplate.queryForObject(query, new UserRowMapper(), username);
}
Suppressing False Positives
For inline suppression, annotate the method with the rule key. For bulk suppression in sonar-project.properties:
sonar.issue.ignore.multicriteria=e1
sonar.issue.ignore.multicriteria.e1.ruleKey=java:S3649
sonar.issue.ignore.multicriteria.e1.resourceKey=/test/
This tells SonarQube to ignore SQL injection findings in test directories where you might intentionally test with raw queries.
---
Python — Bandit and Semgrep
Why Bandit + Semgrep
Bandit is the standard Python security linter — it catches hardcoded passwords, SQL injection, subprocess shells, and insecure deserialization. Semgrep adds custom rules and cross-function taint analysis.
Bandit Installation and Configuration
pip install bandit
Create a .bandit configuration in pyproject.toml:
[tool.bandit]
exclude_dirs = ["tests", "venv", ".venv"]
skips = ["B101"]
targets = ["src"]
[tool.bandit.assert_used]
skips = ["_test.py", "test_.py"]
Run locally:
bandit -r src/ -f json -o bandit-report.json
For a quick severity-filtered scan:
bandit -r src/ -ll -ii
The -ll flag shows only medium severity and above. The -ii flag shows only medium confidence and above.
Semgrep Installation and Custom Rules
pip install semgrep
Run with the default security ruleset:
semgrep scan --config=p/python --config=p/security-audit src/
Create a custom rule in .semgrep/custom-rules.yml:
rules:
- id: flask-debug-enabled
patterns:
- pattern: app.run(..., debug=True, ...)
message: "Flask debug mode must not be enabled in production"
languages: [python]
severity: ERROR
metadata:
category: security
cwe: "CWE-489: Active Debug Code"
- id: unsafe-yaml-load
patterns:
- pattern: yaml.load($X)
- pattern-not: yaml.load($X, Loader=yaml.SafeLoader)
message: "Use yaml.safe_load() or specify Loader=yaml.SafeLoader"
languages: [python]
severity: WARNING
metadata:
category: security
cwe: "CWE-502: Deserialization of Untrusted Data"
Run custom rules:
semgrep scan --config=.semgrep/custom-rules.yml src/
GitHub Actions Step
- name: Run Bandit Security Scan
run: |
pip install bandit
bandit -r src/ -f sarif -o bandit-results.sarif -ll || true
- name: Upload Bandit SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: bandit-results.sarif
- name: Run Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: >-
p/python
p/security-audit
.semgrep/custom-rules.yml
Sample Vulnerable Code
import subprocess
def run_command(user_input):
result = subprocess.Popen(
f"grep {user_input} /var/log/app.log",
shell=True,
stdout=subprocess.PIPE
)
return result.stdout.read()
Bandit reports: B602:subprocess_popen_with_shell_equals_true — Consider possible security implications associated with Popen call. Severity: High. Confidence: High.
The fix avoids shell=True and uses a list:
import subprocess
import shlex
def run_command(user_input):
sanitized = shlex.quote(user_input)
result = subprocess.run(
["grep", sanitized, "/var/log/app.log"],
capture_output=True,
text=True
)
return result.stdout
Suppressing False Positives
Inline suppression for Bandit:
result = subprocess.run(cmd, shell=False) # nosec B603
For Semgrep, use inline comments:
app.run(debug=True) # nosemgrep: flask-debug-enabled
---
JavaScript/TypeScript — ESLint Security Plugins
Why ESLint for Security
ESLint is already in most JS/TS projects. Adding security-focused plugins catches DOM-based XSS, prototype pollution, regex denial-of-service, and unsafe eval usage without introducing a new tool into your workflow.
Installation
npm install --save-dev eslint eslint-plugin-security eslint-plugin-no-unsanitized @typescript-eslint/eslint-plugin @typescript-eslint/parser
Configuration
In .eslintrc.json:
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:security/recommended-legacy"
],
"plugins": ["security", "no-unsanitized"],
"rules": {
"security/detect-object-injection": "warn",
"security/detect-non-literal-regexp": "error",
"security/detect-unsafe-regex": "error",
"security/detect-buffer-noassert": "error",
"security/detect-eval-with-expression": "error",
"security/detect-no-csrf-before-method-override": "error",
"security/detect-possible-timing-attacks": "warn",
"no-unsanitized/method": "error",
"no-unsanitized/property": "error"
},
"overrides": [
{
"files": [".ts", ".tsx"],
"parser": "@typescript-eslint/parser",
"rules": {
"@typescript-eslint/no-explicit-any": "warn"
}
}
]
}
Run locally:
npx eslint src/ --ext .js,.ts,.tsx --format json --output-file eslint-security.json
Node.js Specific Patterns
For server-side Node.js applications, enforce additional rules:
{
"rules": {
"no-eval": "error",
"no-implied-eval": "error",
"no-new-func": "error",
"security/detect-child-process": "warn",
"security/detect-non-literal-fs-filename": "warn"
}
}
These catch patterns like eval(userInput), setTimeout(userString, 0), new Function(userCode), and fs.readFile(userPath).
GitHub Actions Step
- name: Install Dependencies
run: npm ci
- name: ESLint Security Scan
run: |
npx eslint src/ --ext .js,.ts,.tsx \
--format @microsoft/eslint-formatter-sarif \
--output-file eslint-results.sarif
continue-on-error: true
- name: Upload ESLint SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: eslint-results.sarif
Sample Vulnerable Code
// Prototype Pollution
function merge(target, source) {
for (let key in source) {
target[key] = source[key];
}
return target;
}
// ReDoS vulnerability
const emailRegex = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z]{2,4})+$/;
// DOM XSS
element.innerHTML = userInput;
ESLint reports:
security/detect-object-injection— Generic Object Injection Sinksecurity/detect-unsafe-regex— Unsafe regular expressionno-unsanitized/property— Unsafe assignment to innerHTML
Suppressing False Positives
// eslint-disable-next-line security/detect-object-injection
target[knownSafeKey] = value;
Or disable for an entire file when the risk is accepted:
/ eslint-disable security/detect-object-injection /
Always add a comment explaining why suppression is justified.
---
Go — gosec, go vet, and staticcheck
Why gosec for Go
Go's type system prevents many vulnerability classes, but SQL injection, hardcoded credentials, weak crypto, and path traversal still happen. gosec catches these with deep understanding of Go patterns.
Installation
go install github.com/securego/gosec/v2/cmd/gosec@latest
go install honnef.co/go/tools/cmd/staticcheck@latest
Running Locally
gosec -fmt=json -out=gosec-results.json ./...
go vet ./...
staticcheck ./...
gosec Configuration
Create a configuration to focus on specific rules:
gosec -include=G101,G201,G301,G401,G501 -fmt=sarif -out=gosec.sarif ./...
Key rules:
- G101: Hardcoded credentials
- G201: SQL string concatenation
- G301: Poor file permissions on creation
- G401: Use of weak crypto (MD5, SHA1 for security purposes)
- G501: Insecure TLS configuration
GitHub Actions Step
- name: Run gosec Security Scanner
uses: securego/gosec@master
with:
args: '-fmt sarif -out gosec-results.sarif ./...'
- name: Upload gosec SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: gosec-results.sarif
- name: Run staticcheck
uses: dominikh/staticcheck-action@v1
with:
version: "latest"
Sample Vulnerable Code
package main
import (
"crypto/md5"
"database/sql"
"fmt"
"net/http"
)
func getUser(db sql.DB, r http.Request) (*sql.Row, error) {
username := r.URL.Query().Get("user")
query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", username)
return db.QueryRow(query), nil
}
const dbPassword = "supersecret123"
func hashData(data []byte) []byte {
h := md5.Sum(data)
return h[:]
}
gosec reports:
- G201 (CWE-89): SQL string formatting — Severity: MEDIUM, Confidence: HIGH
- G101 (CWE-798): Potential hardcoded credentials — Severity: HIGH, Confidence: LOW
- G401 (CWE-326): Use of weak cryptographic primitive — Severity: MEDIUM, Confidence: HIGH
The fixed version uses parameterized queries, environment variables for secrets, and SHA-256 for hashing:
func getUser(db sql.DB, r http.Request) (*sql.Row, error) {
username := r.URL.Query().Get("user")
return db.QueryRow("SELECT * FROM users WHERE name = $1", username), nil
}
Suppressing False Positives
Use the nosec annotation:
const testToken = "fake-token-for-tests" // #nosec G101 -- test fixture, not real credential
---
C# and .NET — Roslyn Analyzers and SecurityCodeScan
Why Roslyn Analyzers
.NET has first-class security analysis through Roslyn analyzers that run during compilation. You get immediate feedback in your IDE and your build pipeline without separate tooling.
Installation
Add security analyzers to your .csproj:
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0" />
<PackageReference Include="SecurityCodeScan.VS2019" Version="5.6.7" />
<PackageReference Include="Roslynator.Analyzers" Version="4.12.0" />
</ItemGroup>
Or install via CLI:
dotnet add package SecurityCodeScan.VS2019
dotnet add package Microsoft.CodeAnalysis.NetAnalyzers
Configuration
Create a .editorconfig for rule severity:
[*.cs]
dotnet_diagnostic.SCS0002.severity = error
dotnet_diagnostic.SCS0029.severity = error
dotnet_diagnostic.SCS0001.severity = error
dotnet_diagnostic.SCS0010.severity = warning
dotnet_diagnostic.SCS0015.severity = error
dotnet_diagnostic.SCS0016.severity = warning
These rule IDs correspond to:
- SCS0002: SQL Injection
- SCS0029: Cross-Site Scripting (XSS)
- SCS0001: Command Injection
- SCS0010: Weak Cipher Algorithm
- SCS0015: Hardcoded Password
- SCS0016: Cross-Site Request Forgery (CSRF)
Running Locally
dotnet build /p:RunAnalyzersDuringBuild=true
dotnet build /p:ErrorLog=security-results.sarif
GitHub Actions Step
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore and Build with Security Analysis
run: |
dotnet restore
dotnet build --no-restore \
/p:RunAnalyzersDuringBuild=true \
/p:ErrorLog=security-results.sarif \
/warnaserror:SCS0001,SCS0002,SCS0029
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: security-results.sarif
Sample Vulnerable Code
public async Task<User> GetUser(string username)
{
var query = $"SELECT * FROM Users WHERE Name = '{username}'";
return await _context.Users.FromSqlRaw(query).FirstOrDefaultAsync();
}
public string RunDiagnostics(string host)
{
var process = Process.Start("cmd.exe", $"/c ping {host}");
return process.StandardOutput.ReadToEnd();
}
SecurityCodeScan reports:
- SCS0002: SQL Injection vulnerability — using string interpolation in SQL
- SCS0001: Command Injection vulnerability — unsanitized input in process execution
Suppressing False Positives
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Security", "SCS0002:SQL Injection",
Justification = "Input is from internal service, validated upstream")]
public async Task<Report> GetReport(string internalId) { }
---
Cross-Language Pipeline: Putting It All Together
Here is a complete GitHub Actions workflow that runs SAST across a polyglot repository:
name: SAST Security Scan
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
sast-java:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@v2
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
sast-python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Bandit Scan
run: |
pip install bandit
bandit -r src/ -f sarif -o bandit.sarif -ll || true
- name: Upload Bandit SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: bandit.sarif
sast-javascript:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: ESLint Security
run: |
npm ci
npx eslint src/ --ext .js,.ts,.tsx --format json --output-file eslint.json
sast-go:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: gosec Scan
uses: securego/gosec@master
with:
args: '-fmt sarif -out gosec.sarif ./...'
sast-dotnet:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Security Analyzers
run: |
dotnet restore
dotnet build /p:RunAnalyzersDuringBuild=true /warnaserror:SCS0001,SCS0002
---
What Your Manager Wants to See
Security scanning only has organizational value if you can report on it. Here are the metrics that demonstrate your pipeline is working.
Key Metrics to Track
Findings Trend — Total open vulnerabilities over time. Target: decreasing month-over-month.
Fix Rate — Percentage of findings resolved within SLA. Target: greater than 90 percent for CRITICAL, greater than 75 percent for HIGH.
Mean Time to Remediate (MTTR) — Average days from detection to fix. Target: less than 3 days for CRITICAL, less than 14 days for HIGH.
False Positive Rate — Percentage of findings marked as false positive. Target: less than 20 percent. If higher, tune your rules.
New vs. Inherited — New findings introduced in PRs vs. existing technical debt. New findings should trend toward zero.
Coverage — Percentage of repositories with SAST enabled. Target: 100 percent for production services.
Building the Dashboard
Pull data from SonarQube's API:
curl -u "$SONAR_TOKEN:" \
"$SONAR_HOST/api/measures/component?component=my-app&metricKeys=vulnerabilities,security_hotspots,security_rating"
For non-SonarQube tools, parse SARIF output and push counts to your metrics system:
jq '[.runs[].results[] | .level] | group_by(.) | map({level: .[0], count: length})' results.sarif
Monthly Report Template
Present this to leadership monthly:
The goal is not zero findings forever — it is a consistent downward trend with fast remediation when issues appear.
---
Frequently Asked Questions
What is SAST and how does it work?
SAST (Static Application Security Testing) analyzes source code, bytecode, or binaries for security vulnerabilities without executing the application. It examines code paths, data flows, and patterns to identify issues like SQL injection, XSS, hardcoded secrets, and insecure configurations. SAST runs early in development, enabling developers to fix vulnerabilities before deployment.
What are the best SAST tools for DevOps pipelines?
Popular open-source options include Semgrep (multi-language, fast, customizable rules), Bandit (Python), SpotBugs with FindSecBugs (Java), and ESLint security plugins (JavaScript). Commercial options include SonarQube, Checkmarx, and Snyk Code. Choose based on language support, CI/CD integration, and false positive rates. Semgrep offers the best balance of speed and accuracy for most teams.
How do I reduce SAST false positives?
Tune the ruleset to your specific frameworks (e.g., disable SQL injection rules if you use an ORM exclusively), suppress verified false positives with inline annotations, and use severity filters to focus on high-confidence findings. Establish a triage workflow where security champions review and categorize new findings weekly. Custom rules tailored to your codebase produce the best signal-to-noise ratio.
When should I run SAST in the development lifecycle?
Run lightweight SAST checks (Semgrep, ESLint security rules) on every commit in CI — they complete in seconds. Run comprehensive SAST scans nightly or on pull requests to main branches. Provide IDE plugins so developers catch issues before committing. The key principle is fast feedback: scanning should not add more than 2-3 minutes to your pipeline.
What vulnerabilities can SAST detect that DAST cannot?
SAST excels at finding injection flaws (SQL, XSS, command injection), hardcoded credentials, insecure cryptographic implementations, race conditions, buffer overflows, and insecure deserialization patterns. It can trace data flow from user input to dangerous sinks. DAST cannot see code paths that aren't exercised during testing or detect vulnerabilities that require specific input to trigger.
---
Related Resources
- DevOpsKit Prompt Library — 500 DevOps prompts including security scanning automation
- Engineering Toolkit — Tools and scripts for security pipeline setup
- Production Troubleshooting — Scenarios including security incident response
- Secrets Management for DevOps — Complementary guide on managing credentials
- GitHub Actions CI/CD Complete Guide — Pipeline fundamentals for integrating security gates