vulkro-sf antipatterns
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
| Flag | Type | Default | Description |
|---|---|---|---|
--format <fmt> | enum | table | Output 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) | none | Salesforce 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) | none | Compliance 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-cache | flag | off | Disable 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 belowvulkro: ignore AP-029, AP-013- comma-listvulkro: 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)
| Rule | Title | What it flags |
|---|---|---|
| AP-001 | SOQL inside a loop | Any [SELECT ...] query inside a for / while body. Governor-limit risk. |
| AP-002 | DML inside a loop | insert / update / delete / upsert operations inside a loop body. |
| AP-003 | Hard-coded record ID | An 15- or 18-character Salesforce ID embedded as a literal in Apex, LWC, Aura, or Flow. |
| AP-004 | Trigger without bulk-safety | Trigger handlers that operate on Trigger.new[0] instead of iterating the full collection. |
| AP-005 | Recursive trigger pattern | A trigger that performs DML on the same sObject without a static recursion guard. |
| AP-006 | Empty catch block | try { ... } catch (Exception e) { } swallowing every error silently. |
| AP-007 | System.debug left in production code | Debug statements outside a Test.isRunningTest() guard. Performance and PII-leak risk. |
| AP-008 | Schema introspection in a loop | Schema.getGlobalDescribe() or getDescribe() called per iteration. |
| AP-009 | Asynchronous chaining without governor budgeting | Database.executeBatch or System.enqueueJob issued inside a batch class without a chain-depth guard. |
| AP-010 | Missing with sharing on a class that does DML | A non-test Apex class that issues DML and declares neither with sharing nor inherited sharing nor without sharing. |
| AP-011 | Hard-coded URL in Apex | A literal https:// URL inside an Apex string concatenation (use a Named Credential or Custom Metadata Type). |
| AP-012 | Stateless Visualforce controller pattern misuse | A controller declared extends PageReference or that holds large in-memory collections across requests. |
| AP-013 | Aura component with aura:method exposing system context | An Aura controller method that does sharing-bypass work without enforcement comments. |
| AP-014 | Flow with no fault path | A screen or auto-launched Flow that calls a subflow or Apex action without a fault connector. |
| AP-015 | PII field written to a debug log | System.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-016 | Record serialized into a log | System.debug(JSON.serialize(record))-shape call. JSON.serialize emits every field, leaking more than the caller intended. |
| AP-017 | PII copied into an audit or log field | A 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-018 | Agentforce action uses input without validation | An @InvocableMethod (Agentforce-reachable action) feeds an input field into a SOQL bind without an explicit length / regex / allowlist / Id.valueOf check first. |
| AP-023 | Catch-and-rethrow without context | A catch (...) block whose only statement is throw <varname>;. Loses the stack location of the original throw. |
| AP-024 | Partial-success DML without SaveResult inspection | Database.insert(records, false) (or update / delete / upsert variant) whose returned SaveResult list is thrown away. |
| AP-025 | Callout failure swallowed by empty catch | An HttpRequest creation or Http.send followed by catch (...) {} with an empty body. |
| AP-028 | Hardcoded namespace prefix | An identifier of the shape ns__Object__c or ns__Field__c referenced literally. Breaks when the code is repackaged under a different namespace. |
| AP-038 | Custom Apex reimplementing Validation Rule logic | A 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 SOQL | An @AuraEnabled method whose body issues a SOQL query directly rather than going through a selector or service layer. |
| AP-043 | Callout without setTimeout | A method that creates an HttpRequest (or calls Http.send) without setting a timeout on the request. |
| AP-044 | REST endpoint with no auth check | An @HttpGet / @HttpPost / @HttpPut / @HttpDelete / @HttpPatch method that does no UserInfo, Auth., profile, or permission-set check. |
| AP-019 | Agent has broad access to a PII object | A bot version that grants Read / Edit / All access to Contact / Lead / Account / Case / User / Opportunity / PersonAccount. |
| AP-020 | Prompt template interpolates raw input | A {!$Input.var} interpolation in a prompt-template body without an escapeHtml / redact / safe filter. |
| AP-021 | Agent action targets a feed / share / history object | A GenAiFunction whose target object is a platform-internal share / feed / history surface. |
| AP-022 | Agent grounding routes through a Drift-class Connected App | An agent or GenAiFunction grounding source whose Connected App matches Drift / Gainsight / Salesloft / Qualified / GA Pulse class names. |
| AP-026 | Trigger handler swallows exceptions silently | A catch block in a .trigger file that neither logs nor rethrows. |
| AP-027 | Installed managed package on Beta version | <isBeta>true</isBeta> in an InstalledPackage metadata file. |
| AP-029 | Cross-package Apex class referenced without Type.forName | nsName.ClassName.method(...) referenced directly, with no fallback. Excludes system namespaces (System, Database, Schema, etc.). |
| AP-030 | Formula field with deep formula-to-formula chain | A <formula> field that resolves through three or more chained formula references. Computed from object metadata XML. |
| AP-031 | Junction object using two Lookup fields instead of master-detail | An object with exactly two Lookup fields and no MasterDetail. |
| AP-032 | Lookup where master-detail is required by sharing | A Lookup field on an object that also declares Sharing Rules. |
| AP-033 | Flow performs Get Records inside a loop | A Flow loop whose <targetReference> points at a <recordLookups> element. |
| AP-034 | Org has too many profiles | Live-org finding: profile count > 25. |
| AP-035 | Permission-set sprawl | Live-org finding: permission-set count > 50. |
| AP-036 | Connected App sprawl | Live-org finding: Connected App count > 25. |
| AP-037 | Agentforce action / plugin sprawl | Live-org finding: combined Agentforce action + plugin count > 25. |
| AP-039 | Flow reimplementing built-in Approval Process | A Flow with Approve / Reject decision rules and a write to ApprovalStatus__c / Approval_State__c / Approval_Status__c. |
| AP-040 | God 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-042 | Callout without retry path | An HTTP callout in a method that has no surrounding loop and no catch block that re-issues the callout. |
| AP-045 | External Service Registration with no authentication | <authProtocol>NoAuthentication</authProtocol> in an External Service Registration metadata file. |
| AP-046 | Flow runs in System mode against a PII object | <runInMode>SystemModeWithoutSharing</runInMode> + a record-lookup against Contact / Lead / Account / Case / User / Opportunity / PersonAccount. |
| AP-047 | List Custom Setting holds a secret-shaped field | customSettingsType=List + a field named Token / Secret / Password / ApiKey / Credential / etc. |
| AP-048 | Public Custom Metadata Type holds a secret-shaped field | Same pattern against a Custom Metadata Type (__mdt.object-meta.xml) not declared <visibility>Protected</visibility>. |
| AP-049 | PII write without paired delete | A class that writes a known PII field via DML but has no delete / Database.delete / @HttpDelete / merge operation anywhere. |
| AP-050 | LWC / Aura direct DOM write | innerHTML, outerHTML, insertAdjacentHTML, or lwc:dom="manual" in an LWC or Aura JS controller. |
| AP-051 | LWC / Aura eval or new Function | eval(...) or new Function(...) in an LWC or Aura JS controller. |
| AP-052 | Browser localStorage / sessionStorage access | Direct use of window.localStorage, window.sessionStorage, or $A.localStorageService. |
| AP-053 | Visualforce actionPoller below 60-second interval | <apex:actionPoller interval="<60">. |
| AP-054 | Visualforce escape=false on outputText | <apex:outputText escape="false" ...> (XSS sink). |
| AP-055 | Visualforce loads off-platform third-party script | <apex:includeScript value="https://..."> pointing at a host outside the Salesforce CDN. |
| AP-057 | Permission 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-058 | Apex test coverage below AppExchange gate (live-org) | Tooling API query against ApexCodeCoverageAggregate; per-class coverage below 75%. |
| AP-059 | HTTP callout inside a loop | An 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-record | A same-file @future static method called from inside a loop. Apex caps @future at 50 invocations per transaction. |
| AP-071 | System.enqueueJob inside a loop | A Queueable enqueue issued per record. A transaction may add at most 50 jobs to the async queue. |
| AP-072 | Batchable start() query not bounded | A Database.Batchable class whose start() SOQL returns an unbounded QueryLocator (no LIMIT). Advisory. |
| AP-073 | Method consumes a large share of the transaction budget | A 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
- vulkro-sf scan - the security pipeline (a separate command on purpose).
- Output: anti-patterns report -
the format reference for
text,html, andjsonoutput of this command. - Well-Architected anti-patterns concept - the Salesforce framework these rules are drawn from.