SAST Tools Compared: SonarQube vs Semgrep for Modern DevSecOps

SAST Tools Compared: SonarQube vs Semgrep for Modern DevSecOps

Static Application Security Testing (SAST) catches vulnerabilities before they ever run. By analyzing source code at rest, SAST tools identify injection flaws, insecure configurations, hardcoded secrets, and dozens of other issues during development — not after a breach. Two tools dominate modern DevSecOps pipelines: SonarQube and Semgrep. They take fundamentally different approaches, and understanding those differences is the key to choosing the right tool (or combining both).

What SAST Actually Does

SAST tools parse your source code into an abstract syntax tree (AST) or a control-flow graph and run rule sets against that representation. Unlike dynamic testing, SAST requires no running application — it works on pull requests, in pre-commit hooks, or as a CI gate.

The tradeoff: SAST can't see runtime behavior. It won't catch a vulnerability that only appears when a specific API call returns a malformed response. But it will catch the SQL query built with string concatenation before any user ever sends a request.

A well-tuned SAST setup catches:

  • SQL injection, XSS, path traversal, command injection
  • Insecure cryptography (MD5, SHA1, weak key sizes)
  • Hardcoded credentials (though dedicated secrets scanners do this better)
  • Unsafe deserialization
  • Missing authorization checks in common patterns

SonarQube: Enterprise-Grade Static Analysis

SonarQube is the incumbent. It supports 30+ languages, integrates with every major CI system, and ships with thousands of built-in rules covering security, reliability, and maintainability.

Setting Up SonarQube

The fastest path to a working SonarQube instance is Docker:

docker run -d --name sonarqube \
  -p 9000:9000 \
  -e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true \
  sonarqube:community

Default credentials: admin / admin. Change them immediately.

For production, use the official Helm chart:

helm repo add sonarqube https://SonarSource.github.io/helm-chart-sonarqube
helm repo update
helm install sonarqube sonarqube/sonarqube \
  --set postgresql.enabled=true \
  --set postgresql.auth.password=your-db-password

Configuring a Project

Create a sonar-project.properties file in your repo root:

sonar.projectKey=my-app
sonar.projectName=My Application
sonar.sources=src
sonar.tests=tests
sonar.exclusions=**/*.test.js,**/vendor/**,**/node_modules/**
sonar.coverage.exclusions=**/*.test.js

# For JavaScript/TypeScript
sonar.javascript.lcov.reportPaths=coverage/lcov.info

# Security hotspot categories to track
sonar.security.sources.jaas.loginModules=

Run the scanner:

sonar-scanner \
  -Dsonar.host.url=https://sonarqube.your-org.com \
  -Dsonar.token=$SONAR_TOKEN

Understanding Quality Gates

Quality Gates are where SonarQube becomes a real CI gate. The default "Sonar way" gate fails if:

  • Coverage on new code drops below 80%
  • New code has a reliability rating below A
  • New code has a security rating below A
  • New code has a maintainability rating below A

For security-focused teams, tighten the security conditions:

Quality Gate: Security Strict
- New Security Hotspots Reviewed: >= 100%
- New Vulnerabilities: = 0
- New Security Rating: = A

Configure this via the SonarQube UI under Quality Gates, or via the API:

curl -u $SONAR_TOKEN: -X POST \
  "https://sonarqube.your-org.com/api/qualitygates/create" \
  -d "name=Security+Strict"

curl -u $SONAR_TOKEN: -X POST \
  "https://sonarqube.your-org.com/api/qualitygates/create_condition" \
  -d "gateId=2&metric=new_vulnerabilities&op=GT&error=0"

CI/CD Integration

GitHub Actions:

name: SonarQube Analysis
on:
  push:
    branches: [main]
  pull_request:

jobs:
  sonarqube:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for blame data
      
      - name: SonarQube Scan
        uses: SonarSource/sonarqube-scan-action@master
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
      
      - name: SonarQube Quality Gate
        uses: SonarSource/sonarqube-quality-gate-action@master
        timeout-minutes: 5
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

GitLab CI:

sonarqube-check:
  image: sonarsource/sonar-scanner-cli:latest
  stage: security
  variables:
    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
    GIT_DEPTH: "0"
  cache:
    key: "${CI_JOB_NAME}"
    paths:
      - .sonar/cache
  script:
    - sonar-scanner
  allow_failure: false
  only:
    - merge_requests
    - main

Semgrep: Lightweight, Fast, Extensible

Semgrep takes a different approach. Instead of building a full semantic model of your code, it uses pattern matching on a simplified AST. This makes it dramatically faster and makes writing custom rules accessible to developers who aren't compiler engineers.

Installing Semgrep

# pip
pip install semgrep

# Homebrew
brew install semgrep

# Docker
docker pull semgrep/semgrep

Running Your First Scan

# Scan with the security-focused rule pack
semgrep --config=p/security-audit .

# OWASP Top 10 coverage
semgrep --config=p/owasp-top-ten .

# CI-friendly output
semgrep --config=p/ci --json --output=semgrep-results.json .

The p/ prefix pulls from the Semgrep Registry — a public collection of community and Semgrep-maintained rules. For production use, pin specific rule sets rather than pulling latest every run.

Writing Custom Semgrep Rules

This is where Semgrep shines. Rule files are YAML with a pattern DSL that feels approachable:

rules:
  - id: hardcoded-jwt-secret
    patterns:
      - pattern: jwt.sign($PAYLOAD, "...")
      - pattern-not: jwt.sign($PAYLOAD, process.env.$ENV_VAR)
    message: >
      Hardcoded JWT secret detected. Use environment variables or a secrets
      manager instead. Found in $FILE at line $LINE.
    languages: [javascript, typescript]
    severity: ERROR
    metadata:
      category: security
      cwe: CWE-798
      owasp: A07:2021

  - id: unsafe-sql-query
    pattern: |
      $DB.query("..." + $USER_INPUT)
    message: Possible SQL injection via string concatenation
    languages: [javascript, typescript, python]
    severity: ERROR

  - id: missing-rate-limit
    patterns:
      - pattern: |
          app.post($ROUTE, $HANDLER)
      - pattern-not: |
          app.post($ROUTE, rateLimit(...), $HANDLER)
    message: POST endpoint $ROUTE is missing rate limiting middleware
    languages: [javascript]
    severity: WARNING
    paths:
      include:
        - "src/routes/**"

Run your custom rules:

semgrep --config=./security-rules/ --error .

Taint Analysis in Semgrep

Semgrep Pro supports dataflow / taint analysis — tracking user-controlled data from source to sink:

rules:
  - id: taint-sql-injection
    mode: taint
    pattern-sources:
      - pattern: req.body.$FIELD
      - pattern: req.query.$FIELD
      - pattern: req.params.$FIELD
    pattern-sinks:
      - pattern: $DB.query(...)
      - pattern: $DB.execute(...)
    pattern-sanitizers:
      - pattern: $DB.escape(...)
      - pattern: parameterized($QUERY, ...)
    message: User input flows to SQL query without sanitization
    languages: [javascript, typescript]
    severity: ERROR

Head-to-Head Comparison

Dimension SonarQube Semgrep
Languages 30+ 30+
Rule count (built-in) 5,000+ 3,000+ (registry)
Custom rule complexity Java/XML-based YAML pattern DSL
Scan speed (10k LOC) ~2-5 min ~10-30 sec
False positive rate Medium-High Low-Medium
Self-hosted cost Free (Community) Free (OSS)
Taint analysis Yes (paid) Yes (Pro)
PR decoration Yes Yes
Historical tracking Yes Limited
Code coverage integration Yes No

Managing False Positives

False positives are the main reason SAST adoption fails. Developers learn to ignore the scanner when every result is noise.

SonarQube suppression:

// Within code — use sparingly
@SuppressWarnings("java:S2077") // Suppresses SQL injection check
public List<User> search(String query) {
    // This is safe because query is validated by the InputValidator
    return db.execute("SELECT * FROM users WHERE " + validatedQuery);
}

Or mark as "Won't Fix" / "False Positive" in the SonarQube UI, which persists across scans.

Semgrep suppression:

// nosemgrep: hardcoded-jwt-secret
const testSecret = "test-secret-for-unit-tests-only";

Or use .semgrepignore:

# Ignore test fixtures
tests/fixtures/
tests/mocks/

# Ignore generated code
generated/
dist/

Systematic false positive reduction:

  1. Track your false-positive rate per rule. If a rule fires > 50% false positives, tune or disable it.
  2. Create rule exceptions for test directories globally — test code has different security requirements.
  3. Review suppression comments in PRs to catch misuse.
  4. Re-evaluate suppressed findings quarterly.

Use both. They complement each other:

  • Semgrep in pre-commit and PR checks — fast, low friction, catches obvious issues before review
  • SonarQube as the authoritative gate — deeper analysis, historical trending, code coverage correlation

In your CI pipeline, run Semgrep first (it's faster). If Semgrep passes, SonarQube runs its deeper analysis. This keeps feedback loops tight while maintaining thorough coverage.

For monitoring the full security testing picture across your CI runs — including SAST gate pass rates over time — HelpMeTest can track your pipeline health and alert when security checks start degrading, giving you visibility beyond what a single scanner dashboard provides.

Practical Rule Management

For teams starting out, avoid the temptation to enable every rule. Start with:

SonarQube: Enable the "Sonar way" security profile, then add your top 3 language-specific OWASP rules.

Semgrep: Start with p/owasp-top-ten and p/secrets. Add custom rules only when you identify a gap.

Review your active rules quarterly. Rules that have never fired in 90 days are either already-mitigated risk (good) or dead rules that generate false confidence (investigate).

# Semgrep: list rules that fired in last scan
semgrep --config=p/security-audit --json . | \
  jq '[.results[] | .check_id] | unique | sort'

# Compare against your enabled rule list to find never-firing rules

SAST is not a one-time setup. It's an ongoing practice: tuning rules as your codebase evolves, reviewing suppressions, and treating the quality gate as a real gate — not a checkbox that's always bypassed. The teams that get value from SAST are the ones that have a process for acting on its findings, not just running it.

Read more

Start now free