Skip to main content

vulkro-sf antipatterns

Pro

vulkro-sf antipatterns is part of Pro: Anti-patterns (sf-antipatterns), the Well-Architected review, run over the project. On Free it stops before doing any work, prints what it needs and where to get it, and exits 1. See Pricing.

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 or any SARIF-aware dashboard), 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