Testland
Browse all skills & agents

pmd-apex-rules

Runs PMD's built-in Apex security ruleset (`category/apex/security.xml`) against Salesforce Apex source to detect injection, privilege-escalation, cryptographic, and XSS vulnerabilities; configures custom rulesets for regulated-industry Apex codebases; emits SARIF for GitHub Code Scanning upload; integrates `pmd check` as a PR-blocking CI gate. Use when the codebase contains Salesforce Apex and the team needs SAST coverage for ApexSOQLInjection, ApexCRUDViolation, ApexSharingViolations, or the full 10-rule security category.

Install with skills.sh (any agent)

npx skills add testland/qa --skill pmd-apex-rules
View source

pmd-apex-rules

Overview

Per pmd.github.io - Apex Security Rules (opens in new window):

PMD ships a built-in category/apex/security.xml ruleset that covers 10 security rules for Salesforce Apex. All 10 rules are present since PMD 5.5.3 and carry Medium (3) priority by default. The ruleset addresses the Salesforce-specific threat model: SOQL injection, object/field-level security bypass, sharing-model evasion, hard-coded credentials, insecure endpoints, XSS through Visualforce, and open redirect.

This skill is the Apex-specific companion to semgrep-rules and sonarqube-rules. Those tools cover general multi-language patterns; this one covers the Salesforce Apex security category absent from both. Findings from all five SAST tools can be unified in a multi-scanner triage step.

When to use

  • The repo contains Salesforce Apex (.cls, .trigger) and requires SAST gating in CI.
  • Regulated-industry teams (financial services, healthcare, government) need documented evidence of permission checks and sharing-mode enforcement.
  • The team already runs PMD on Java and wants a consistent runner for Apex.
  • Security review asks for SOQL injection, CRUD/FLS bypass, or sharing violation detection.

Step 1 - Install

Per pmd.github.io - Installation (opens in new window), PMD requires Java 8 or later. Download the zip from the GitHub releases page (opens in new window), unzip, and add bin/ to PATH:

# Linux / macOS
unzip pmd-dist-*.zip -d ~/pmd
export PATH="$HOME/pmd/bin:$PATH"

# Verify
pmd --version

For CI, prefer the Docker image or the Maven/Gradle plugin to avoid zip management. The Docker image is available at ghcr.io/pmd/pmd.

Step 2 - First scan with the built-in security ruleset

Per pmd.github.io - CLI Reference (opens in new window):

pmd check -d . -R category/apex/security.xml -f sarif -r pmd-apex.sarif

Flag reference (per pmd-cli (opens in new window)):

FlagMeaning
-d <path>Source directory or file to analyze
-R <refs>Ruleset path; comma-separated for multiple
-f <format>Output format (sarif, text, xml, json, html; default: text)
-r <file>Write report to file instead of stdout
--minimum-priority <n>Skip rules below priority n (1=High, 5=Info)
--cache <file>Enable incremental analysis (per pmd-cache (opens in new window))

Exit codes (per pmd-cli (opens in new window)):

CodeMeaning
0Success, no violations
1Unhandled exception
2Invalid arguments
4Violations detected
5Recoverable parsing errors

Step 3 - The 10 Apex security rules

The category/apex/security.xml ruleset ships 10 rules. The headline three are ApexSOQLInjection (dynamic SOQL built from untrusted input), ApexCRUDViolation (missing object/field permission check before SOQL/SOSL/DML), and ApexSharingViolations (DML without an explicit sharing keyword). The full 10-rule table and per-rule compliant/non-compliant examples are in references/apex-security-rules.md, per pmd-apex-sec (opens in new window).

Step 4 - Custom ruleset (subset or extended)

Per pmd.github.io - Making Rulesets (opens in new window):

Use a custom XML ruleset to select a subset, override priorities, or add exclusion patterns for generated code:

<?xml version="1.0"?>
<ruleset name="Apex Security - Regulated"
    xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0
    https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
  <description>Apex security rules for regulated-industry Apex</description>

  <!-- Include the full security category -->
  <rule ref="category/apex/security.xml">
    <!-- Suppress for auto-generated WSDL stubs -->
    <exclude name="ApexSuggestUsingNamedCred"/>
  </rule>

  <!-- Exclude generated code directories -->
  <exclude-pattern>.*/generated/.*</exclude-pattern>
</ruleset>

Run with the custom ruleset:

pmd check -d force-app/main/default/classes \
          -R config/pmd-apex-regulated.xml \
          -f sarif \
          -r pmd-apex.sarif

Per pmd-rulesets (opens in new window), referencing an entire category means the ruleset automatically picks up new rules added to that category in future PMD versions. Pin specific versions in CI to avoid unexpected gate changes.

Step 5 - Incremental analysis for faster CI

Per pmd-cache (opens in new window) (PMD 5.6.0+):

pmd check -d force-app/main/default/classes \
          -R category/apex/security.xml \
          -f sarif \
          -r pmd-apex.sarif \
          --cache .pmd-cache/apex.cache

The cache stores file checksums. Unchanged files reuse cached results; only modified files are re-analyzed. The generated report is identical to a full run (per pmd-cache (opens in new window)). Cache is invalidated automatically on PMD version change, ruleset modification, or auxclasspath change.

Step 6 - CI gate (GitHub Actions)

jobs:
  pmd-apex:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Download PMD
        run: |
          PMD_VERSION=7.7.0
          curl -Lo pmd.zip \
            https://github.com/pmd/pmd/releases/download/pmd_releases%2F${PMD_VERSION}/pmd-dist-${PMD_VERSION}-bin.zip
          unzip -q pmd.zip -d pmd-dist
          echo "$PWD/pmd-dist/pmd-bin-${PMD_VERSION}/bin" >> $GITHUB_PATH

      - name: Run PMD Apex security scan
        run: |
          pmd check \
            -d force-app/main/default/classes \
            -R category/apex/security.xml \
            -f sarif \
            -r pmd-apex.sarif \
            --cache .pmd-cache/apex.cache
        # Exit code 4 = violations found (per pmd-cli); gate blocks on non-zero
        continue-on-error: false

      - name: Upload SARIF to GitHub Code Scanning
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: pmd-apex.sarif
          category: pmd-apex

Per pmd-cli (opens in new window), exit code 4 means violations detected. continue-on-error: false blocks the PR on any finding. Use --minimum-priority 2 to gate only on High and Critical findings while still uploading all findings via SARIF.

Step 7 - Suppression

PMD suppression in Apex (per pmd-apex-sec (opens in new window)): annotate the method or class with @SuppressWarnings:

@SuppressWarnings('PMD.ApexCRUDViolation')
public class VisualforceGetter {
    // Visualforce getters auto-enforce FLS; CRUD check is redundant here
    public List<Account> getAccounts() {
        return [SELECT Id, Name FROM Account];
    }
}

Add a comment explaining the justification. Suppressions without rationale are flagged in code review.

Anti-patterns

Anti-patternWhy it failsFix
Running against all directories including classes/testTest classes generate false positives for CRUD/sharingExclude test directories with <exclude-pattern>
No --cache in CI on large orgsFull re-scan of 500+ classes on every commit is slowAdd --cache .pmd-cache/apex.cache (Step 5)
Suppressing ApexSOQLInjection globallyMasks real injection risksSuppress per method with a written justification only
Floating latest PMD version in CIGate breaks when a new rule fires unexpectedlyPin the PMD_VERSION variable
Custom auth facade without *AuthMethodPattern configAll CRUD-checked methods still flagged as violationsConfigure createAuthMethodPattern etc. per pmd-apex-sec (opens in new window)

Limitations

  • PMD Apex analysis is syntactic, not data-flow-based; it can miss taint paths that cross method boundaries. For deep interprocedural analysis pair with codeql-queries.
  • ApexCRUDViolation generates false positives on Visualforce getter methods where FLS is enforced automatically; suppress with justification per Step 7.
  • PMD does not parse Lightning Web Components (.js); client-side XSS is out of scope. Use semgrep-rules with the p/owasp-top-ten ruleset for LWC JavaScript.

References

PMD Apex security rules

View source (opens in new window)

PMD Apex security rules

The 10-rule category/apex/security.xml catalog plus per-rule detail for pmd-apex-rules. All entries per pmd.github.io - Apex Security Rules (opens in new window).

RuleWhat it detects
ApexSOQLInjectionDynamic SOQL/DML built by string concatenation with untrusted input
ApexCRUDViolationMissing object/field permission check before SOQL, SOSL, or DML
ApexSharingViolationsClasses performing DML without an explicit sharing keyword
ApexBadCryptoHard-coded IVs or keys in cryptographic operations
ApexDangerousMethodsCalls to Configuration.disableTriggerCRUDSecurity() or sensitive System.debug()
ApexInsecureEndpointPlain HTTP (non-HTTPS) callout endpoints
ApexOpenRedirectRedirects using unsanitized user-controlled input
ApexSuggestUsingNamedCredHard-coded credentials in HTTP headers; suggests Named Credentials
ApexXSSFromEscapeFalseaddError() called with escape disabled, exposing raw user content
ApexXSSFromURLParamURL parameters used in output contexts without escaping

ApexSOQLInjection

Per pmd-apex-sec (opens in new window): "Detects the usage of untrusted / unescaped variables in DML queries."

Non-compliant:

public class Foo {
    public void test1(String t1) {
        Database.query('SELECT Id FROM Account' + t1);
    }
}

Compliant (bind variable - automatically sanitized by the Apex runtime):

public class Foo {
    public void test1(String accountName) {
        List<Account> accounts = [SELECT Id FROM Account WHERE Name = :accountName];
    }
}

ApexCRUDViolation

Per pmd-apex-sec (opens in new window): "The rule validates you are checking for access permissions before a SOQL/SOSL/DML operation." Accepted remediation paths include DescribeSObjectResult system checks, WITH SECURITY_ENFORCED, or (since Winter '23 / API v56) WITH USER_MODE.

The rule is configurable for custom authorization facades via regex properties (createAuthMethodPattern, readAuthMethodPattern, etc.) so teams using an internal ESAPI wrapper can still pass the check.

ApexSharingViolations

Per pmd-apex-sec (opens in new window): "Detect classes declared without explicit sharing mode if DML methods are used." The three accepted keywords are with sharing, without sharing, and inherited sharing. The intent is to force a conscious declaration of sharing posture, not to mandate a specific value.

Related skills

bandit-python

Configures and runs Bandit - Python-only SAST covering 60+ rule IDs across 7 categories (B1xx-B7xx: misc, app, crypto, imports, injections, XSS); `bandit -r .` scan, `--severity-level` + `--confidence-level` filtering, `# nosec`/`# nosec B404` per-line and per-rule suppression, `pyproject.toml [tool.bandit]` config. Use for a focused, low-overhead Python SAST in pre-commit / CI. Python-only: for Go use gosec-go, for cross-language pattern SAST use semgrep-rules; to merge Bandit findings with other scanners into one gate use multi-tool-finding-triage - not this for non-Python code.

codeql-queries

Configures and runs GitHub CodeQL - semantic-database SAST with queries written in the CodeQL declarative query language; supports `codeql database create` (per-language) + `codeql database analyze` with --format=sarif; ships query packs (`codeql/javascript-queries`, `codeql/python-queries`, `codeql/java-queries`, `codeql/go-queries`, etc.); integrates with GitHub Code Scanning via SARIF upload; suppression via inline comment + sarif-filter + Security-tab dismissal. Use when the team uses GitHub-hosted repos and needs deep semantic SAST beyond pattern matching (cross-file taint flows, dataflow analysis).

eslint-security-rules

Configures and runs `eslint-plugin-security` (14 detect-* rules covering injection, path traversal, ReDoS, unsafe buffers, and bidi trojan-source) plus `eslint-plugin-no-unsanitized` (DOM XSS via `innerHTML`, `outerHTML`, `document.write`, `insertAdjacentHTML`) as the JS/TS first-party SAST layer; covers flat config setup, per-rule suppression with justification templates, SARIF output via `@microsoft/eslint-formatter-sarif` for GitHub Code Scanning upload, and CI gating on ESLint exit code 1. Use when the project is JS or TS and needs an in-process security lint pass without a separate SAST server.

gosec-go

Configures and runs gosec - Go-only SAST covering 40+ rule IDs (G101 hardcoded creds, G104 unhandled errors, G304 path traversal, G401 weak crypto, G601 memory aliasing) via Go AST + SSA taint tracking; `gosec ./...` scan, `#nosec G404 -- justification` suppression, `--fmt sarif|json|junit-xml|html`, golangci-lint integration. Use for a focused Go SAST wired into golangci-lint / CI. Go-only: for Python use bandit-python, for cross-language pattern SAST use semgrep-rules; to merge gosec findings with other scanners into one gate use multi-tool-finding-triage - not this for non-Go code.

multi-tool-finding-triage

Merges two or more security scanner reports into one gate. Use when you need a single BLOCK or PASS decision from multiple scanners instead of reading N separate reports. Normalizes each report into one common finding format (a canonical `Finding`), deduplicates on a per-domain key while recording which scanners agree (`caught_by` consensus), validates a waiver (finding-suppression) file, rejecting any missing `expires:` / `approved_by:` / `reason:` or expired, enriches CVE findings with EPSS (exploit-probability) and CISA KEV (known-exploited catalog), then applies a `fail_on` severity threshold to emit BLOCK or PASS plus a bucketed pull-request comment. Works across static (SAST), dynamic (DAST), secret, dependency (SCA), container, and IaC scanners. To run a single scanner instead use semgrep-rules, codeql-queries, bandit-python, or gosec-go; this runs after them to merge output - the cross-scanner gate, not a single-scanner wrapper.

semgrep-rules

Configures and runs Semgrep - pattern-based SAST across 30+ languages with the Semgrep Registry rulesets (`p/owasp-top-ten`, `p/default`, `auto`) plus custom YAML rules; integrates `semgrep ci` for PR-blocking gates with `--baseline-commit` diff-aware scanning, per-finding inline `nosemgrep` suppressions, `--exclude` / `--include` path filters, output formats (`--json` / `--sarif` / `--gitlab-sast` / `--junit-xml`), and severity filter (INFO/WARNING/ERROR). Use when the user runs Semgrep, asks about pattern rules, or needs a low-friction SAST gate without semantic-DB setup.

sonarqube-rules

Configures and runs SonarQube / SonarCloud - multi-language SAST + Quality Gate platform with built-in Sonar Way rule profiles + custom rule plugins; integrates `sonar-scanner` with `sonar-project.properties` config; supports Quality Gate definitions including new-code-period blocking, branch + PR analysis, and per-issue suppression via `// NOSONAR` comment or `@SuppressWarnings("squid:RULE_ID")` annotation. Use when the user runs SonarQube Community / Developer / Enterprise edition or SonarCloud, or needs a multi-language SAST + code-quality platform with persistent issue tracking.