Static analysis for Apex is an unsolved problem in most Salesforce shops. Salesforce’s built-in Code Analyser exists but is heavyweight, requires Java, and produces output that’s difficult to integrate into a CI pipeline without post-processing. I wanted something simpler: a single Python command that scans staged Apex files and fails the build when it finds something dangerous.
apex-security-scanner is the result. It maps every finding to an OWASP Top 10 category, outputs clean terminal or JSON results, and runs in under a second on a typical feature branch.
What It Detects
The scanner covers six vulnerability categories drawn from OWASP Top 10 and the Salesforce-specific extensions to it:
SOQL injection - dynamic SOQL built with string concatenation using user-controlled input. The classic pattern is Database.query('SELECT Id FROM Account WHERE Name = \'' + userInput + '\''). The scanner detects variable interpolation in Database.query() and [SELECT ...] dynamic blocks.
Hardcoded credentials - API keys, passwords, and tokens embedded directly in Apex. Common patterns include assignments to variables named apiKey, password, token, secret, and key with string literals. Also flags @RemoteAction methods that expose authentication material.
Missing CRUD/FLS checks - DML operations (insert, update, delete, upsert) that aren’t preceded by a Schema.sObjectType.isCreateable() / isUpdateable() / isDeletable() check. Without these, Apex code ignores the user’s object-level permissions, which is an access control bypass.
Unsafe DML in loops - insert, update, delete inside for loops, which causes governor limit exceptions at scale and is consistently flagged in security reviews.
Dangerous sharing model - classes declared without sharing that perform DML or SOQL. This bypasses Salesforce’s record-level security (sharing rules) entirely.
Open redirects - PageReference or ApexPages.currentPage().getParameters() used with unchecked user input in redirect targets.
Architecture
The scanner is a pure Python CLI with no dependencies beyond the standard library. Each rule is a class inheriting from a base Rule type:
class Rule:
id: str # e.g. "A03-SOQL-INJECTION"
owasp: str # e.g. "A03:2021 – Injection"
severity: str # HIGH | MEDIUM | LOW
description: str
def check(self, lines: list[str], filepath: str) -> list[Finding]:
...
The scanner walks each .cls file line by line and runs every enabled rule. Findings collect into a list and are rendered either as terminal output (with colour via ANSI codes) or as JSON for CI consumption.
apex-security-scanner/
├── apex_security_scanner/
│ ├── rules/
│ │ ├── soql_injection.py
│ │ ├── hardcoded_credentials.py
│ │ ├── crud_fls.py
│ │ ├── dml_in_loop.py
│ │ ├── sharing_model.py
│ │ └── open_redirect.py
│ ├── scanner.py # walks files, runs rules, aggregates findings
│ ├── reporter.py # terminal + JSON output
│ └── cli.py # argparse entry point
└── pyproject.toml
OWASP Mapping
Every finding includes its OWASP Top 10 category. This isn’t just labelling - it makes the output actionable in organisations that use OWASP as a risk framework:
| Rule | OWASP Category |
|---|---|
| SOQL Injection | A03:2021 – Injection |
| Hardcoded Credentials | A02:2021 – Cryptographic Failures |
| Missing CRUD/FLS | A01:2021 – Broken Access Control |
| Unsafe DML in Loops | A04:2021 – Insecure Design |
| Dangerous Sharing Model | A01:2021 – Broken Access Control |
| Open Redirect | A01:2021 – Broken Access Control |
Running It
pip install apex-security-scanner
# Scan a directory
apex-scan ./force-app/main/default/classes/
# Scan specific files
apex-scan AccountTriggerHandler.cls OpportunityService.cls
# JSON output for CI
apex-scan ./force-app --format json --output findings.json
# Fail on HIGH severity only
apex-scan ./force-app --min-severity HIGH
CI Integration
The tool is designed to run in a pre-commit hook or a GitHub Actions step. Exit code 0 means clean; non-zero means findings at or above the configured minimum severity.
In GitHub Actions:
- name: Apex security scan
run: |
pip install apex-security-scanner
apex-scan force-app/main/default/classes/ --min-severity MEDIUM
In a pre-commit hook (using the .githooks/pre-commit pattern):
staged=$(git diff --cached --name-only --diff-filter=ACM | grep '\.cls$')
if [ -n "$staged" ]; then
apex-scan $staged --min-severity HIGH
fi
Limitations and What’s Next
The current scanner is AST-free - it uses regex and line-level pattern matching rather than a full Apex parser. This means:
- Some patterns require lookahead (e.g. checking if a CRUD check exists anywhere in the same method)
- It produces false positives on complex multi-line expressions
- It won’t detect indirect injection through method calls that pass user data through layers
A proper Apex AST parser would solve these at the cost of significantly more complexity. The current approach works well as a first-pass CI gate - it catches the most common issues fast, and the false positive rate is low enough on real codebases to be useful without constant tuning.
The project is open source on GitHub.