Signed rules-bundle distribution
Salesforce ships Agentforce metadata changes on a multi-week cadence, and the third-party reviewers track new managed-package customer- verification actions, new Trusted URL wildcards, and new PII surfaces as they appear. Vulkro for Salesforce v0.1.3 ships a bundle channel that lets the detection rules keep up without requiring a binary release for every catalog change.
How it works
The scanner consults a knowledge bundle every time it runs the Agentforce / planner / topic / action checks (AP-061..AP-070). The bundle is a single JSON file with the catalog data the rules need:
- Standard managed-package customer-verification actions and their required outputs.
- Word-count thresholds for topic master labels, descriptions, and scope strings.
- The per-planner excessive-topic threshold.
- File-name patterns that count as test files for an agent / topic / action.
- PII-bearing free-text fields the ForcedLeak class can flow through.
- Trusted URL wildcard patterns the platform has removed from the default allowlist.
Every binary ships with a seed copy of this bundle baked in. The seed is the canonical fallback: a scan never blocks on a missing or unreachable bundle, and an air-gapped install always has a usable catalog out of the box.
On a network-connected install, the scanner asks the seed if it has
checked for an update in the last 24 hours. If not, it makes a single
HTTPS GET to
https://dist.vulkro.com/sf/rules/agentforce.bundle.json plus a
sibling GET to agentforce.bundle.sig. The signature is verified
against an ed25519 public key baked into the binary at compile time.
A signature mismatch is treated as a tamper event: the fetched bundle
is rejected and the seed catalog is used.
Why it's signed
The threat model is "an attacker who controls the CDN can poison the
catalog." A bundle without a signature would let that attacker swap
in their own catalog: they could quietly add their domain to the
trusted_url_wildcard_blocklist removed list to silence a finding,
or remove a PII field from the catalog to silence a different one.
The signature defends against that exactly. The public key is binary-pinned: an attacker who compromises the CDN cannot also swap the verification key, because that key lives in the customer's local binary. Key rotation requires a new binary release that bumps the pinned constant, which is the right control: a key compromise should not be self-recoverable from a bundle update.
The private signing key never ships and never travels. It lives only
on the release-signing host. The signing CLI
(vulkro-sign-rules-bundle) is operator-side, never shipped to
customers.
What the rules read from the bundle
| Finding | Catalog field |
|---|---|
| AP-061 (PII free-text into agent) | pii_free_text_fields |
| AP-062 (over-broad Trusted URL) | trusted_url_wildcard_blocklist |
| AP-066 (missing customer verification) | standard_verification_actions |
| AP-067 (missing output assignments) | standard_verification_actions.required_outputs |
| AP-068 (insufficient topic instructions) | topic_word_thresholds |
| AP-069 (excessive topics) | excessive_topics_threshold |
| AP-070 (untested agent action) | test_file_patterns |
AP-063, AP-064, AP-018/024/042/044, AP-056..AP-058, and AP-060 do not consult the bundle; they are pure-code or pure-metadata heuristics and ship in the binary.
Operator controls
The fetch path honours four environment variables:
VULKRO_SF_OFFLINE=1skips the fetch entirely. The scanner uses the seed catalog regardless of whether a refresh is due. Recommended for air-gapped CI lanes and for customers who want zero outbound HTTPS from the scanner.VULKRO_SF_RULES_BUNDLE_OVERRIDE=<path>sideloads a local JSON file as the catalog instead of fetching. Useful when testing a candidate bundle before publishing.VULKRO_SF_RULES_BUNDLE_URL=<base>overrides the base URL. Default ishttps://dist.vulkro.com/sf/rules. Useful for air-gapped mirrors that proxy the production CDN.VULKRO_SF_RULES_BUNDLE_CACHE_DIR=<path>overrides the on-disk cache directory. Default is~/.vulkro/rules-bundle/.VULKRO_SF_RULES_BUNDLE_MIN_REFRESH_SECS=<n>overrides the 24-hour refresh cadence. Clamped to[60, 604800](1 minute to 7 days). To disable updates entirely, setVULKRO_SF_OFFLINE=1rather than pushing this value to the ceiling.
When the seed wins
The seed catalog wins (the scanner ignores the fetched bundle) when:
VULKRO_SF_OFFLINE=1is set.- The fetch fails (DNS error, TLS handshake fail, 404, 5xx).
- The signature does not verify against the pinned public key.
- The JSON does not parse (
#[serde(deny_unknown_fields)]is on, so a bundle that adds an unknown top-level key is rejected). - The cache directory cannot be created or written to.
In each case the scanner emits a single INFO-level line on stderr
naming the failure mode (remote Agentforce bundle unavailable (...); using seed) so an operator can decide whether to investigate. An
offline-by-policy install logs nothing; a SHA mismatch logs the
mismatch and uses the seed.
What the wire format looks like
GET https://dist.vulkro.com/sf/rules/agentforce.bundle.json
GET https://dist.vulkro.com/sf/rules/agentforce.bundle.sig
The .json is a UTF-8 JSON document conforming to the
AgentforceKnowledge schema. The .sig is a raw 64-byte detached
ed25519 signature over the JSON's bytes. No base64, no hex, no
envelope: the bytes are exactly what the customer-side fetcher reads.
The same shape ships in v0.1.3 binaries today. Future schema additions
will use #[serde(default)] so an older customer install can still
parse a newer bundle without rejecting it.
Where to go next
- Agentforce and AI for the rule catalog the bundle drives.
- CLI reference: scan for the environment variables.
- Methodology for the full scanner architecture.