Skip to main content

vulkro-sf antipatterns

Runs the Salesforce Well-Architected anti-pattern catalog over the provided path. These rules cover architecture quality (governor-limit risk, hard-coded IDs, recursive triggers, batch context misuse) rather than security. The output is intentionally separate from vulkro-sf scan so the security report does not get diluted with maintainability findings, and so a consultancy team can run the anti-pattern pass on a different cadence from the security pass.

Synopsis

vulkro-sf antipatterns [PATH] [flags]

PATH defaults to the current directory.

Flags

FlagTypeDefaultDescription
--format <fmt>enumtableOutput format: table (terminal summary), json (machine-readable findings for a triage tool), sarif (SARIF v2.1.0 - GitHub Security tab / SonarQube / Codacy compatible), or junit (one <testcase> per finding so CI runners surface them as failed tests).
--exclude-namespace <ns>string (repeatable, comma-separated)noneSalesforce namespace(s) the project considers its own, in addition to anything sfdx-project.json declares. AP-028 / AP-029 suppress matches against these. Use for legacy / merged-package projects: --exclude-namespace=npe01,npe03,npo02 for NPSP.
--compliance <profile>enum (repeatable, comma-separated)noneCompliance profile(s) that broaden AP-015 / AP-016 / AP-017 PII detection. hipaa adds the 18 HIPAA Safe Harbor identifier classes (medical record number, biometric ID, device serial, IP / URL, etc.). pci adds PCI-DSS card-data identifiers (PAN, CVV, expiry, cardholder name, magnetic-stripe). Stack with a comma: --compliance=hipaa,pci.
--no-cacheflagoffDisable the per-file detector cache and force a full re-scan. The cache lives at ~/.vulkro/sf-antipattern-cache.json and keys on (source SHA-256, detector version, compliance flags, own-namespace set). Equivalent to VULKRO_SF_NO_CACHE=1.
--cache-dir <path>path~/.vulkro/Override the directory the cache lives in. Useful in CI runners where the cache should be scoped to the workspace. Equivalent to VULKRO_SF_CACHE_DIR=....

Project-level config

The scanner also reads .vulkro-sf.yml at (or above) the scan root. Two fields are honoured:

disable:
- AP-013 # silence a noisy detector for this project
extra_excluded_namespaces:
- npe01 # merge with --exclude-namespace; same semantics
- npe03

Malformed YAML logs a warning and the scan continues with defaults. Findings disabled via disable: are dropped from the output before the cache write, so a later scan with disable: cleared re-emits them without re-running detectors.

Suppress comments

Drop a single finding without disabling its detector:

// vulkro: ignore AP-029
partnerpkg.Notifier.send(account.Id, 'created');

Forms supported (in both Apex // comments and XML <!-- --> comments):

  • vulkro: ignore AP-029 - silence a single detector on the same line or the line directly below
  • vulkro: ignore AP-029, AP-013 - comma-list
  • vulkro: ignore * - wildcard, silences any finding on that line

The directive is matched case-insensitively. It does not affect cache state, so a later commit that removes the directive re-emits the finding on the next scan.

Rule list (AP-001 to AP-045)

RuleTitleWhat it flags
AP-001SOQL inside a loopAny [SELECT ...] query inside a for / while body. Governor-limit risk.
AP-002DML inside a loopinsert / update / delete / upsert operations inside a loop body.
AP-003Hard-coded record IDAn 15- or 18-character Salesforce ID embedded as a literal in Apex, LWC, Aura, or Flow.
AP-004Trigger without bulk-safetyTrigger handlers that operate on Trigger.new[0] instead of iterating the full collection.
AP-005Recursive trigger patternA trigger that performs DML on the same sObject without a static recursion guard.
AP-006Empty catch blocktry { ... } catch (Exception e) { } swallowing every error silently.
AP-007System.debug left in production codeDebug statements outside a Test.isRunningTest() guard. Performance and PII-leak risk.
AP-008Schema introspection in a loopSchema.getGlobalDescribe() or getDescribe() called per iteration.
AP-009Asynchronous chaining without governor budgetingDatabase.executeBatch or System.enqueueJob issued inside a batch class without a chain-depth guard.
AP-010Missing with sharing on a class that does DMLA non-test Apex class that issues DML and declares neither with sharing nor inherited sharing nor without sharing.
AP-011Hard-coded URL in ApexA literal https:// URL inside an Apex string concatenation (use a Named Credential or Custom Metadata Type).
AP-012Stateless Visualforce controller pattern misuseA controller declared extends PageReference or that holds large in-memory collections across requests.
AP-013Aura component with aura:method exposing system contextAn Aura controller method that does sharing-bypass work without enforcement comments.
AP-014Flow with no fault pathA screen or auto-launched Flow that calls a subflow or Apex action without a fault connector.
AP-015PII field written to a debug logSystem.debug(...) whose argument references a Salesforce PII field (Email, Phone, MobilePhone, mailing or billing address, SSN, date of birth). Logs land in the Setup audit trail.
AP-016Record serialized into a logSystem.debug(JSON.serialize(record))-shape call. JSON.serialize emits every field, leaking more than the caller intended.
AP-017PII copied into an audit or log fieldA custom field whose name contains Audit / History / Log / Snapshot / Payload receives a value from a known PII field. Audit columns are normally plain Long Text Area, not Shield-encrypted.
AP-018Agentforce action uses input without validationAn @InvocableMethod (Agentforce-reachable action) feeds an input field into a SOQL bind without an explicit length / regex / allowlist / Id.valueOf check first.
AP-023Catch-and-rethrow without contextA catch (...) block whose only statement is throw <varname>;. Loses the stack location of the original throw.
AP-024Partial-success DML without SaveResult inspectionDatabase.insert(records, false) (or update / delete / upsert variant) whose returned SaveResult list is thrown away.
AP-025Callout failure swallowed by empty catchAn HttpRequest creation or Http.send followed by catch (...) {} with an empty body.
AP-028Hardcoded namespace prefixAn identifier of the shape ns__Object__c or ns__Field__c referenced literally. Breaks when the code is repackaged under a different namespace.
AP-038Custom Apex reimplementing Validation Rule logicA method that calls .addError(...) three or more times. Multiple addError calls usually mean a Validation Rule would do the same job declaratively.
AP-041@AuraEnabled with inline SOQLAn @AuraEnabled method whose body issues a SOQL query directly rather than going through a selector or service layer.
AP-043Callout without setTimeoutA method that creates an HttpRequest (or calls Http.send) without setting a timeout on the request.
AP-044REST endpoint with no auth checkAn @HttpGet / @HttpPost / @HttpPut / @HttpDelete / @HttpPatch method that does no UserInfo, Auth., profile, or permission-set check.
AP-019Agent has broad access to a PII objectA bot version that grants Read / Edit / All access to Contact / Lead / Account / Case / User / Opportunity / PersonAccount.
AP-020Prompt template interpolates raw inputA {!$Input.var} interpolation in a prompt-template body without an escapeHtml / redact / safe filter.
AP-021Agent action targets a feed / share / history objectA GenAiFunction whose target object is a platform-internal share / feed / history surface.
AP-022Agent grounding routes through a Drift-class Connected AppAn agent or GenAiFunction grounding source whose Connected App matches Drift / Gainsight / Salesloft / Qualified / GA Pulse class names.
AP-026Trigger handler swallows exceptions silentlyA catch block in a .trigger file that neither logs nor rethrows.
AP-027Installed managed package on Beta version<isBeta>true</isBeta> in an InstalledPackage metadata file.
AP-029Cross-package Apex class referenced without Type.forNamensName.ClassName.method(...) referenced directly, with no fallback. Excludes system namespaces (System, Database, Schema, etc.).
AP-030Formula field with deep formula-to-formula chainA <formula> field that resolves through three or more chained formula references. Computed from object metadata XML.
AP-031Junction object using two Lookup fields instead of master-detailAn object with exactly two Lookup fields and no MasterDetail.
AP-032Lookup where master-detail is required by sharingA Lookup field on an object that also declares Sharing Rules.
AP-033Flow performs Get Records inside a loopA Flow loop whose <targetReference> points at a <recordLookups> element.
AP-034Org has too many profilesLive-org finding: profile count > 25.
AP-035Permission-set sprawlLive-org finding: permission-set count > 50.
AP-036Connected App sprawlLive-org finding: Connected App count > 25.
AP-037Agentforce action / plugin sprawlLive-org finding: combined Agentforce action + plugin count > 25.
AP-039Flow reimplementing built-in Approval ProcessA Flow with Approve / Reject decision rules and a write to ApprovalStatus__c / Approval_State__c / Approval_Status__c.
AP-040God method (SOQL + DML + control flow)A method body that contains a SOQL query, a DML statement, and at least one of if / for / while / switch.
AP-042Callout without retry pathAn HTTP callout in a method that has no surrounding loop and no catch block that re-issues the callout.
AP-045External Service Registration with no authentication<authProtocol>NoAuthentication</authProtocol> in an External Service Registration metadata file.
AP-046Flow runs in System mode against a PII object<runInMode>SystemModeWithoutSharing</runInMode> + a record-lookup against Contact / Lead / Account / Case / User / Opportunity / PersonAccount.
AP-047List Custom Setting holds a secret-shaped fieldcustomSettingsType=List + a field named Token / Secret / Password / ApiKey / Credential / etc.
AP-048Public Custom Metadata Type holds a secret-shaped fieldSame pattern against a Custom Metadata Type (__mdt.object-meta.xml) not declared <visibility>Protected</visibility>.
AP-049PII write without paired deleteA class that writes a known PII field via DML but has no delete / Database.delete / @HttpDelete / merge operation anywhere.
AP-050LWC / Aura direct DOM writeinnerHTML, outerHTML, insertAdjacentHTML, or lwc:dom="manual" in an LWC or Aura JS controller.
AP-051LWC / Aura eval or new Functioneval(...) or new Function(...) in an LWC or Aura JS controller.
AP-052Browser localStorage / sessionStorage accessDirect use of window.localStorage, window.sessionStorage, or $A.localStorageService.
AP-053Visualforce actionPoller below 60-second interval<apex:actionPoller interval="<60">.
AP-054Visualforce escape=false on outputText<apex:outputText escape="false" ...> (XSS sink).
AP-055Visualforce loads off-platform third-party script<apex:includeScript value="https://..."> pointing at a host outside the Salesforce CDN.
AP-057Permission Set Group over-privilege (live-org)A PSG that bundles 10+ permission sets, or whose aggregated risk is critical/high. Lives on the live-org snapshot.
AP-058Apex test coverage below AppExchange gate (live-org)Tooling API query against ApexCodeCoverageAggregate; per-class coverage below 75%.
AP-059HTTP callout inside a loopAn Http.send, new HttpRequest, .send(req), or Database.getRecords / getQueryLocator callout issued inside a for / while body. Each iteration consumes one of the 100 callouts-per-transaction limit.
AP-065@future method invoked per-recordA same-file @future static method called from inside a loop. Apex caps @future at 50 invocations per transaction.
AP-071System.enqueueJob inside a loopA Queueable enqueue issued per record. A transaction may add at most 50 jobs to the async queue.
AP-072Batchable start() query not boundedA Database.Batchable class whose start() SOQL returns an unbounded QueryLocator (no LIMIT). Advisory.
AP-073Method consumes a large share of the transaction budgetA single method body that statically issues more than 15 SOQL queries or more than 15 DML statements. Intra-method advisory, distinct from the in-loop rules AP-001 / AP-002.

The Resource Optimiser group collects the governor-limit findings (AP-001, AP-002, AP-008, AP-013, AP-040, AP-042, AP-059, AP-065, AP-071, AP-072, AP-073). In --format table output each of these is tagged (Resource Optimiser) so the "manage your governor limits" findings read as one coherent set. The tag is display-only: the pillar in --format json / sarif / junit is unchanged (always Trusted (Reliable)).

Out of scope (process-level, not detector candidates)

These six Salesforce Well-Architected anti-pattern areas are inherently process-level: Governance, KPIs, In-App Guidance, ALM / Environment Strategy, Triage / Incident Response, and Backup-and-Restore / Continuity Planning, plus Accessibility / Data Entry (mostly UX). Vulkro for Salesforce does not ship detectors for these areas. They live in your organisation's governance practice, not in source code.

Examples

# Full anti-pattern pass over an SFDX project. Table summary.
vulkro-sf antipatterns .

# JSON output, piped to a file for a dashboard or triage tool.
vulkro-sf antipatterns . --format json > antipatterns.json

# Anti-pattern pass over a specific path (e.g. the Apex source folder of
# a multi-package monorepo).
vulkro-sf antipatterns ./force-app/main/default

# SARIF for the GitHub Security tab (or any SARIF-aware scanner sink).
vulkro-sf antipatterns . --format sarif > findings.sarif

# JUnit XML for CI runners (GitHub Actions, GitLab CI, Jenkins).
vulkro-sf antipatterns . --format junit > findings.xml

# NPSP-style merged-package project: silence the legacy namespaces.
vulkro-sf antipatterns ./npsp --exclude-namespace=npe01,npe03,npo02,npe4

# Healthcare / payments coverage broadens AP-015..017 PII detection.
vulkro-sf antipatterns . --compliance=hipaa
vulkro-sf antipatterns . --compliance=hipaa,pci

# Force a full re-scan (skip the cache).
vulkro-sf antipatterns . --no-cache

# Scope the cache to the CI workspace so it persists between runs of
# the same pipeline but doesn't leak across branches / projects.
vulkro-sf antipatterns . --cache-dir "$CI_PROJECT_DIR/.vulkro-cache"

Exit codes

  • 0 - scan completed, no anti-patterns found.
  • 1 - scan completed, anti-patterns were reported.
  • 2 - error: bad arguments, IO failure, parse error, or internal crash.

Where to go next