SAST: Static Application Security Testing in Practice
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 dependenciesSAST vs Other Security Testing
| Approach | When | What | Coverage |
|---|---|---|---|
| SAST | Build time | Source code | 100% codebase |
| DAST | Runtime | Running app | Exposed endpoints |
| IAST | Runtime | Instrumented app | Executed paths |
| SCA | Build time | Dependencies | Third-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: bridgeProject 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 rulesBasic 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: alwaysCustom 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 onlyHandling 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 suppressMetrics 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 timeDashboard 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
-
Layer your tools: Use multiple SAST tools for comprehensive coverage—each has different strengths
-
Integrate early: Run SAST in IDE (SonarLint) for immediate developer feedback
-
Quality gates are essential: Block deployments that don't meet security thresholds
-
Custom rules add value: Write organisation-specific rules for your security patterns
-
Manage false positives carefully: Document suppressions and review them regularly
-
Track metrics: Measure vulnerability trends and remediation times to improve over time
-
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. \