Skip to main content
Back to Blog
6 December 202513 min read

SAST: Static Application Security Testing in Practice

SecurityDevSecOpsSASTCode Quality

Master static application security testing with practical implementations using SonarQube, Semgrep, and CodeQL. Learn to identify vulnerabilities before code reaches production.


Static Application Security Testing (SAST) analyses source code to identify security vulnerabilities without executing the application. This guide covers practical implementation of SAST tools in modern development workflows.

Understanding SAST

What SAST Analyses

SAST Analysis Scope:
├── Source Code
│   ├── Business logic flaws
│   ├── Injection vulnerabilities
│   ├── Authentication issues
│   └── Cryptographic weaknesses
├── Configuration Files
│   ├── Hardcoded credentials
│   ├── Insecure settings
│   └── Exposed secrets
├── Infrastructure as Code
│   ├── Terraform misconfigurations
│   ├── CloudFormation issues
│   └── Kubernetes manifests
└── Dependencies (overlaps with SCA)
    ├── Direct dependencies
    └── Transitive dependencies

SAST vs Other Security Testing

ApproachWhenWhatCoverage
SASTBuild timeSource code100% codebase
DASTRuntimeRunning appExposed endpoints
IASTRuntimeInstrumented appExecuted paths
SCABuild timeDependenciesThird-party code

SonarQube Implementation

Architecture Overview

SonarQube Architecture:
┌─────────────────────────────────────────────┐
│                Developer IDE                 │
│            (SonarLint Plugin)               │
└─────────────────┬───────────────────────────┘
                  │ Real-time feedback
                  ▼
┌─────────────────────────────────────────────┐
│              CI/CD Pipeline                  │
│         (SonarScanner Analysis)             │
└─────────────────┬───────────────────────────┘
                  │ Results
                  ▼
┌─────────────────────────────────────────────┐
│            SonarQube Server                  │
├─────────────────────────────────────────────┤
│  ┌─────────┐  ┌─────────┐  ┌─────────┐     │
│  │ Web     │  │ Compute │  │ Search  │     │
│  │ Server  │  │ Engine  │  │ Engine  │     │
│  └────┬────┘  └────┬────┘  └────┬────┘     │
│       └────────────┼────────────┘           │
│                    ▼                        │
│            ┌───────────────┐                │
│            │   Database    │                │
│            │  (PostgreSQL) │                │
│            └───────────────┘                │
└─────────────────────────────────────────────┘

Docker Compose Setup

# docker-compose.yml version: '3.8' services: sonarqube: image: sonarqube:lts-community container_name: sonarqube depends_on: - db environment: SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar SONAR_JDBC_USERNAME: sonar SONAR_JDBC_PASSWORD: sonar SONAR_ES_BOOTSTRAP_CHECKS_DISABLE: "true" volumes: - sonarqube_data:/opt/sonarqube/data - sonarqube_extensions:/opt/sonarqube/extensions - sonarqube_logs:/opt/sonarqube/logs ports: - "9000:9000" networks: - sonarnet ulimits: nofile: soft: 65536 hard: 65536 db: image: postgres:15-alpine container_name: sonarqube-db environment: POSTGRES_USER: sonar POSTGRES_PASSWORD: sonar POSTGRES_DB: sonar volumes: - postgresql_data:/var/lib/postgresql/data networks: - sonarnet volumes: sonarqube_data: sonarqube_extensions: sonarqube_logs: postgresql_data: networks: sonarnet: driver: bridge

Project Configuration

# sonar-project.properties sonar.projectKey=my-application sonar.projectName=My Application sonar.projectVersion=1.0.0 # Source configuration sonar.sources=src sonar.tests=tests sonar.exclusions=**/node_modules/**,**/dist/**,**/*.spec.ts # Language-specific settings sonar.typescript.lcov.reportPaths=coverage/lcov.info sonar.javascript.lcov.reportPaths=coverage/lcov.info # Quality gate sonar.qualitygate.wait=true sonar.qualitygate.timeout=300 # Security hotspots sonar.security.hotspots.review.priority=HIGH,MEDIUM # Branch analysis (requires Developer Edition) # sonar.branch.name=${BRANCH_NAME}

GitHub Actions Integration

# .github/workflows/sonarqube.yml name: SonarQube Analysis on: push: branches: [main, develop] pull_request: branches: [main] jobs: sonarqube: name: SonarQube Scan runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 # Full history for blame information - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install dependencies run: npm ci - name: Run tests with coverage run: npm run test:coverage - name: SonarQube Scan uses: SonarSource/sonarqube-scan-action@master env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} with: args: > -Dsonar.projectKey=my-application -Dsonar.sources=src -Dsonar.tests=tests -Dsonar.typescript.lcov.reportPaths=coverage/lcov.info - name: SonarQube Quality Gate uses: SonarSource/sonarqube-quality-gate-action@master timeout-minutes: 5 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

Semgrep Implementation

Why Semgrep

Semgrep Advantages:
├── Speed: Fast pattern matching
├── Simplicity: Easy rule creation
├── Accuracy: Low false positive rate
├── Coverage: 30+ languages supported
├── Free: Open source core
└── Customisable: Write your own rules

Basic Configuration

# .semgrep.yml rules: - id: hardcoded-secret-in-code patterns: - pattern-either: - pattern: $KEY = "..." - pattern: $KEY = '...' pattern-inside: | ... $KEY = $VALUE ... metavariable-regex: metavariable: $KEY regex: (?i)(password|secret|api_key|token|credential) message: Potential hardcoded secret detected severity: ERROR languages: [python, javascript, typescript, java] - id: sql-injection-risk patterns: - pattern-either: - pattern: $QUERY = f"... {$VAR} ..." - pattern: $QUERY = "..." + $VAR + "..." - pattern: $QUERY = \\\`... ${$VAR} ...\\\` metavariable-regex: metavariable: $QUERY regex: (?i)(select|insert|update|delete|drop) message: Potential SQL injection vulnerability severity: ERROR languages: [python, javascript, typescript] - id: insecure-crypto-algorithm pattern-either: - pattern: crypto.createHash('md5') - pattern: crypto.createHash('sha1') - pattern: hashlib.md5(...) - pattern: hashlib.sha1(...) message: Weak cryptographic algorithm detected severity: WARNING languages: [javascript, typescript, python]

Custom Rules for TypeScript

# semgrep-rules/typescript-security.yml rules: - id: ts-unsafe-eval patterns: - pattern-either: - pattern: eval($X) - pattern: new Function($X) - pattern: setTimeout($X, ...) - pattern: setInterval($X, ...) message: | Avoid using eval() or Function constructor with dynamic input. This can lead to code injection vulnerabilities. severity: ERROR languages: [typescript, javascript] metadata: cwe: CWE-94 owasp: A03:2021 - id: ts-xss-vulnerability patterns: - pattern-either: - pattern: element.innerHTML = $X - pattern: document.write($X) - pattern: $EL.dangerouslySetInnerHTML = { __html: $X } message: | Setting innerHTML directly can lead to XSS vulnerabilities. Use textContent or sanitise input before rendering. severity: ERROR languages: [typescript, javascript] metadata: cwe: CWE-79 owasp: A03:2021 - id: ts-prototype-pollution patterns: - pattern: $OBJ[$KEY] = $VALUE - metavariable-regex: metavariable: $KEY regex: (__proto__|constructor|prototype) message: Potential prototype pollution vulnerability severity: ERROR languages: [typescript, javascript] - id: ts-insecure-randomness patterns: - pattern: Math.random() message: | Math.random() is not cryptographically secure. Use crypto.randomBytes() or crypto.getRandomValues() for security-sensitive operations. severity: WARNING languages: [typescript, javascript]

GitHub Actions with Semgrep

# .github/workflows/semgrep.yml name: Semgrep Security Scan on: push: branches: [main, develop] pull_request: branches: [main] schedule: - cron: '0 2 * * 1' # Weekly on Monday jobs: semgrep: name: Semgrep Scan runs-on: ubuntu-latest container: image: semgrep/semgrep steps: - name: Checkout code uses: actions/checkout@v4 - name: Run Semgrep run: | semgrep scan \\\\ --config auto \\\\ --config .semgrep.yml \\\\ --config p/security-audit \\\\ --config p/secrets \\\\ --config p/owasp-top-ten \\\\ --sarif \\\\ --output semgrep-results.sarif \\\\ --error \\\\ --severity ERROR - name: Upload SARIF results uses: github/codeql-action/upload-sarif@v3 with: sarif_file: semgrep-results.sarif if: always() semgrep-pr-comment: name: Semgrep PR Analysis runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - name: Checkout code uses: actions/checkout@v4 - name: Semgrep PR Scan uses: semgrep/semgrep-action@v1 with: config: >- auto p/security-audit p/secrets generateSarif: "1" env: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}

CodeQL Implementation

CodeQL Configuration

# .github/codeql/codeql-config.yml name: "Custom CodeQL Configuration" disable-default-queries: false queries: - uses: security-extended - uses: security-and-quality - uses: ./custom-queries query-filters: - exclude: id: js/unused-local-variable - include: severity: error severity: warning paths: - src - lib paths-ignore: - node_modules - dist - '**/*.test.ts' - '**/*.spec.ts'

GitHub Actions CodeQL Workflow

# .github/workflows/codeql.yml name: CodeQL Analysis on: push: branches: [main, develop] pull_request: branches: [main] schedule: - cron: '0 3 * * 0' # Weekly on Sunday jobs: analyze: name: Analyze runs-on: ubuntu-latest timeout-minutes: 30 permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: ['javascript-typescript', 'python'] steps: - name: Checkout repository uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} config-file: .github/codeql/codeql-config.yml queries: +security-extended,+security-and-quality - name: Autobuild uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: "/language:${{ matrix.language }}" upload: always

Custom CodeQL Query

// custom-queries/jwt-weak-secret.ql /** * @name JWT signed with weak secret * @description Signing JWTs with weak secrets can allow attackers to forge tokens * @kind problem * @problem.severity error * @security-severity 8.0 * @precision high * @id js/jwt-weak-secret * @tags security * external/cwe/cwe-347 */ import javascript import DataFlow class JwtSignCall extends DataFlow::CallNode { JwtSignCall() { exists(DataFlow::ModuleImportNode jwt | jwt.getPath() = "jsonwebtoken" and this = jwt.getAMemberCall("sign") ) } DataFlow::Node getSecret() { result = this.getArgument(1) } } class WeakSecret extends DataFlow::Node { WeakSecret() { // String literals less than 32 characters exists(StringLiteral s | this.asExpr() = s and s.getValue().length() < 32 ) or // Common weak secrets exists(StringLiteral s | this.asExpr() = s and s.getValue().toLowerCase().regexpMatch("(secret|password|key|test|dev).*") ) } } from JwtSignCall signCall, WeakSecret weakSecret where signCall.getSecret() = weakSecret select signCall, "JWT signed with potentially weak secret"

CI/CD Pipeline Integration

Complete Security Pipeline

# .github/workflows/security-pipeline.yml name: Security Pipeline on: push: branches: [main, develop] pull_request: branches: [main] env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} jobs: # Stage 1: Quick static analysis quick-scan: name: Quick Security Scan runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Secret Detection uses: trufflesecurity/trufflehog@main with: path: ./ base: ${{ github.event.repository.default_branch }} extra_args: --only-verified - name: Semgrep Quick Scan uses: semgrep/semgrep-action@v1 with: config: p/ci # Stage 2: Comprehensive SAST sast-analysis: name: SAST Analysis needs: quick-scan runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install dependencies run: npm ci - name: Run tests with coverage run: npm run test:coverage - name: SonarQube Scan uses: SonarSource/sonarqube-scan-action@master - name: Quality Gate Check uses: SonarSource/sonarqube-quality-gate-action@master timeout-minutes: 5 # Stage 3: CodeQL deep analysis codeql-analysis: name: CodeQL Analysis needs: quick-scan runs-on: ubuntu-latest permissions: security-events: write steps: - uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: javascript-typescript queries: +security-extended - name: Autobuild uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 # Stage 4: Security gate security-gate: name: Security Gate needs: [sast-analysis, codeql-analysis] runs-on: ubuntu-latest steps: - name: Check security results run: | echo "All security scans passed" echo "Ready for deployment"

Quality Gates and Thresholds

SonarQube Quality Gate Configuration

{ "qualityGate": { "name": "Security-Focused Gate", "conditions": [ { "metric": "new_security_rating", "op": "GT", "error": "1" }, { "metric": "new_reliability_rating", "op": "GT", "error": "1" }, { "metric": "new_security_hotspots_reviewed", "op": "LT", "error": "100" }, { "metric": "new_coverage", "op": "LT", "error": "80" }, { "metric": "new_duplicated_lines_density", "op": "GT", "error": "3" }, { "metric": "new_vulnerabilities", "op": "GT", "error": "0" } ] } }

Security Rating Definitions

SonarQube Security Ratings:
├── A (1.0): No vulnerabilities
├── B (2.0): At least 1 minor vulnerability
├── C (3.0): At least 1 major vulnerability
├── D (4.0): At least 1 critical vulnerability
└── E (5.0): At least 1 blocker vulnerability

Severity Levels:
├── Blocker: Immediate fix required
├── Critical: Must fix before release
├── Major: Should fix soon
├── Minor: Can defer
└── Info: Awareness only

Handling False Positives

SonarQube Annotations

// Suppress specific issue (use sparingly) // NOSONAR: This is intentionally using eval for dynamic plugin loading const plugin = eval(pluginCode); // Better approach: use @SuppressWarnings /** * @SuppressWarnings("typescript:S1172") * Parameter 'unused' is required by interface contract */ function handler(event: Event, unused: Context): void { // Implementation }

Semgrep Annotations

// Inline ignore // nosemgrep: typescript-security.ts-unsafe-eval const result = eval(trustedExpression); // Block ignore /* nosemgrep */ function legacyCode() { // Multiple issues here that are accepted tech debt }

False Positive Management Strategy

Managing False Positives:
├── Document: Always comment why it's a false positive
├── Review: Require security team approval for suppressions
├── Track: Log suppressions in security register
├── Revisit: Review suppressions quarterly
└── Minimise: Improve rules rather than suppress

Metrics and Reporting

Key SAST Metrics

SAST KPIs:
├── Vulnerabilities Found
│   ├── By severity (Critical/High/Medium/Low)
│   ├── By type (Injection/XSS/Auth/Crypto)
│   └── By component
├── Remediation Metrics
│   ├── Mean time to fix (MTTF)
│   ├── Fix rate per sprint
│   └── Backlog age
├── Quality Metrics
│   ├── False positive rate
│   ├── Coverage percentage
│   └── Technical debt ratio
└── Process Metrics
    ├── Scan time
    ├── Gate pass rate
    └── Developer feedback time

Dashboard Configuration

// security-dashboard.ts interface SecurityMetrics { vulnerabilities: { critical: number; high: number; medium: number; low: number; }; trends: { newIssues: number; fixedIssues: number; netChange: number; }; coverage: { linesAnalysed: number; totalLines: number; percentage: number; }; } async function getSecurityDashboard(): Promise<SecurityMetrics> { const sonarMetrics = await fetchSonarQubeMetrics(); const codeqlAlerts = await fetchCodeQLAlerts(); const semgrepFindings = await fetchSemgrepFindings(); return aggregateMetrics([ sonarMetrics, codeqlAlerts, semgrepFindings ]); }

Key Takeaways

  1. Layer your tools: Use multiple SAST tools for comprehensive coverage—each has different strengths

  2. Integrate early: Run SAST in IDE (SonarLint) for immediate developer feedback

  3. Quality gates are essential: Block deployments that don't meet security thresholds

  4. Custom rules add value: Write organisation-specific rules for your security patterns

  5. Manage false positives carefully: Document suppressions and review them regularly

  6. Track metrics: Measure vulnerability trends and remediation times to improve over time

  7. Balance speed and depth: Use quick scans for PRs, deep analysis on schedules

SAST is foundational to DevSecOps—it catches vulnerabilities before they reach production. Combined with SCA and DAST, it forms a comprehensive security testing strategy. \

Share this article