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
| 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 / SonarQube / Codacy compatible), 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.