Baselines
Every real Salesforce codebase has findings that the team has deliberately accepted: in-flight migrations, third-party code the team cannot rewrite, tactical workarounds that have a documented deprecation date. Running a scan against the whole project for the first time produces a list that mixes those accepted findings with the new ones the latest change introduced; the noise drowns the signal.
A baseline is the accepted-findings snapshot. After a baseline
is in place, vulkro-sf scan --baseline baseline.json only reports
net-new findings that did not exist in the baseline. The team
gates CI on net-new only, which is the right shape: the build does
not fail on debt the team has already accepted, and any new PR is
held to the higher standard.
When you want a baseline
- The first scan on a mature codebase. The full pass produces hundreds of findings; the engineering team's first job is to triage. A baseline freezes the current set; the next scan only surfaces new ones.
- AppExchange resubmission gating. When the first submission to Salesforce Security Review fails on a known set of findings, the ISV fixes those, then baselines the new state. Subsequent revisions can only introduce net-new findings; anything from the baseline that re-appears is a regression and gates the build.
- Consultancy engagement scoring. A consultancy that audits a customer's org at engagement start can baseline the findings on day one and report net-new vs net-fixed at engagement end as the engagement deliverable.
Generating a baseline
vulkro-sf scan writes the current scan's findings as a baseline
file when you pass the --write-baseline flag:
vulkro-sf scan ./force-app --write-baseline baseline.json
This produces baseline.json containing one entry per finding: the
rule ID, the file path, the line number, the message, and a stable
baseline hash that fingerprints the finding's content. The
baseline hash is line-tolerant: a fix or refactor that moves the
finding up or down in the file does not invalidate the entry; only
content-changing edits do.
Commit the baseline alongside the source. The recommended location
is .vulkro/baseline.json at the repo root, so the same baseline
ships with the project across machines and runners.
Scanning against a baseline
Subsequent scans pass --baseline:
vulkro-sf scan ./force-app --baseline .vulkro/baseline.json
The scan runs every detector as normal, then subtracts anything present in the baseline. The text output prints only the net-new findings; the SARIF, JSON, and HTML outputs do the same. The exit code follows the standard contract on the net-new set:
0: scan completed and no net-new findings were reported.1: at least one net-new finding was reported.2: error.
A finding that disappears between the baseline and the current
scan is a fix and does not gate. By default fixes are silent. Pass
--report-fixed to print the closed findings; this is the right
shape for the engagement-end deliverable.
vulkro-sf scan ./force-app \
--baseline .vulkro/baseline.json \
--report-fixed
Updating the baseline as you fix things
Over time the baseline should shrink. Two patterns work:
Pattern 1: fix the finding, drop the line
After fixing a finding and confirming the next scan no longer emits it, the baseline entry for that finding is stale (it points at a file location that no longer matches). Re-write the baseline:
vulkro-sf scan ./force-app --write-baseline baseline.json
This produces a fresh baseline reflecting the new current state. Commit it. The next PR scan now treats the shrunken set as the accepted floor.
Pattern 2: explicit drop list
When the team has agreed to actively work through a subset of the baseline, write that subset's rule IDs to a drop list and pass:
vulkro-sf scan ./force-app \
--baseline .vulkro/baseline.json \
--baseline-drop sf_metadata::guest_user_object_view_all
The baseline entries for that rule ID are excluded from the acceptance set; any remaining instance fires as net-new and gates CI.
AppExchange resubmission pattern
The end-to-end flow for an ISV using baselines to manage Security Review revisions:
- First submission. Run a full scan; do not use a baseline. Fix every finding the team intends to fix before submitting. Some are accepted as documented exceptions in the submission package.
- Submission fails with N findings. Salesforce returns a list of failing items.
- Fix the failures, then run
vulkro-sf scan --write-baseline baseline.jsonto capture the now-clean state. - Iterate on the package. Each subsequent revision is scanned
with
--baseline baseline.json. Only net-new findings gate the next submission. - Re-submit. The clean run plus the readiness report (AppExchange readiness report) are the artifacts that go in the submission package.
The baseline becomes the contract: every revision must preserve the closed-finding set and not introduce regressions.
Consultancy engagement pattern
The end-to-end flow for a consultancy or internal IT-security team auditing a customer org:
-
Day 1 (engagement start). Run a full scan on the customer's source plus a live-org pass (see source vs live org). Write the baseline:
vulkro-sf scan ./force-app \--write-baseline customer-baseline.json -
Throughout the engagement. Every change the consultancy ships runs against the baseline. The team only sees new regressions, not the inherited backlog.
-
Day 60 (engagement end). Run the engagement-end scan with
--report-fixed:vulkro-sf scan ./force-app \--baseline customer-baseline.json \--report-fixedThe output is a paired list: net-new findings (the regressions the team introduced, if any) plus net-fixed findings (the work the engagement closed). This is the deliverable: a measurable scope-of-work artifact that the customer signs off on.
Caveats
- Baselines are file-and-content scoped, not line-scoped. A refactor that splits one file into two will produce new findings in the new file paths even though the underlying issue is unchanged. Update the baseline after such refactors.
- Baselines do not justify the accepted findings. A long-lived
baseline file with thousands of entries is a smell. The
recommended discipline: keep the baseline small, and document
the rationale for each retained finding in a sibling
baseline-notes.md. - Severity changes do not invalidate baseline entries. If a detector tightens and bumps a Medium finding to High in a future Vulkro release, the existing baseline entry still matches and suppresses it. Re-write the baseline after a detector-release bump to surface the upgraded findings for re-triage.
Where to go next
- CI/CD integration: the baseline-and-net-new pattern is the standard CI gate; the CI page shows the wiring per provider.
- AppExchange readiness report: pair the baseline with the readiness HTML for the submission artifact.
- Methodology: the master coverage matrix so you can decide which findings the baseline should retain and which the team intends to actively work through.