NEW @W-23659201@ Add uibundle engine to Code Analyzer Core - #499
NEW @W-23659201@ Add uibundle engine to Code Analyzer Core#499amritmishra-sf wants to merge 22 commits into
Conversation
Introduces `@salesforce/code-analyzer-uibundle-engine`, a new SFCA v5 engine plugin that validates UI Bundle build output. Named generically so additional UI-bundle rule families can be added later without a package rename. Initial ruleset (8 rules) covers sourcemap-integrity: missing sourcemap, path leakage, invalid source references, VLQ integrity, source content verification, coverage analysis, structural coherence, token consistency. Whitelists the new package in .node-scripts/validate-changed-package-versions.js since it has not yet been published to the registry.
|
Git2Gus App is installed but the |
Covers what the engine is for, when to use it, how bundle targets are detected, and a per-rule reference for all 8 rules including how each one works, why it matters, and the constants/thresholds involved.
Adds the new @salesforce/code-analyzer-uibundle-engine plugin to the CLI's EnginePluginsFactoryImpl so it runs alongside the other engines with `sf code-analyzer run`. Depends on forcedotcom/code-analyzer-core#499 being merged and the engine package being published before this PR's CI can go green.
aruntyagiTutu
left a comment
There was a problem hiding this comment.
Reviewed the new code-analyzer-uibundle-engine package (engine.ts, plugin.ts, messages.ts, rules.ts, all 8 validators, and tests). Solid first cut — async I/O throughout (no sync fs calls), good use of caching (sourceAstCache, embeddedCache), tests have real assertions and cover error paths (missing sourcemap, bad VLQ, leaked paths, missing source refs), and the PR description accurately matches what's implemented (8 rules, goldfile test, companion PR sequencing noted).
A few non-blocking suggestions:
-
Redundant source-tree indexing across validators (perf).
source-content-verification.ts,structural-coherence.ts, andtoken-consistency.tseach independently walk + read + index the entire source tree via their ownindexSourceFiles(). When all three rules run together (the default), the source tree gets walked and every file re-read/re-indexed 3x. Per the team's "minimize passes over data" guideline, consider hoisting the source index intorunOnTarget()inengine.tsand passing a shared index into each validator, so it's built once per target regardless of how many source-dependent rules are selected. -
Duplicated helper code.
indexSourceFiles,expandIndexWithBase, andINDEX_IGNORE_PREFIXESare copy-pasted verbatim betweenstructural-coherence.tsandtoken-consistency.ts(and a near-identical variant lives insource-content-verification.ts). Worth extracting intosourcemap-io.tsas a shared utility — would also make suggestion #1 easier to implement in one place. -
findNodeAtOffset's early break (source-content-verification.ts) assumesnodes[]is sorted bybyteOffset. That holds today becausecollectSignificantNodesrelies on Babel's enter-order traversal producing non-decreasing start offsets, but it's an implicit invariant, not asserted or documented at the call site. If that ever changes (e.g. a traversal tweak), thebreakwould silently drop valid matches rather than erroring. A short comment noting the sortedness assumption (or an explicit sort before this loop) would make it safer to modify later. -
Minor:
DANGEROUS_API_PATTERNSbuilds"eval("and"Function("via["ev","al","("].join("")style construction with no comment explaining why — presumably to avoid this scanner's own dangerous-pattern list from tripping other static-analysis tools on itself. A one-line comment would save the next reader some head-scratching.
None of these block merge — nice addition to the engine lineup.
| @@ -0,0 +1,14 @@ | |||
| BSD 3-Clause License | |||
There was a problem hiding this comment.
is the license file present for all engines ?
There was a problem hiding this comment.
yes, it seems like other engines have this too
Automated review — Code Analyzer team standardsReviewed against the team's PR review standards (performance, naming, validation, logging, testing, compatibility, cross-platform, dependencies). Checked out the PR branch and read every changed source/test/doc file directly. Overall verdict: Changes RequestedPer this team's own trigger list, two things independently qualify: a performance-sensitive hot path shipped with zero large-project measurement, and a correctness bug in the Critical-severity rule that causes false positives. 🔴 BlockingPerformance —
Correctness —
Performance —
Correctness — Windows path separators (duplicated bug in both files)
Math bug —
Test suite — vacuous assertions
Architecture — DRY violation
🟡 Medium (worth resolving before merge)
🟢 Low / nits
✅ Clean
Bottom line: solid first cut of a new engine with good structural conventions (sibling-package parity, message catalog pattern, async I/O throughout), but the three biggest sourcemap-analysis validators ( Generated via automated review against the Code Analyzer team's PR standards (604 review comments / 339 merged PRs analysis). |
…ce-content-verification severity Skips webpack/vite/?raw virtual pseudo-sources when checking whether a mapped AST node's source is present on disk, matching the existing byte-equal gate. Fixes false-positive "not present in the submitted source tree" findings for GraphQL ?raw imports and other bundler virtuals in clean bundles. Also drops source-content-verification from Critical to High so all Layer-1 gating rules share the same severity, and updates the goldfile to reflect the reduced tag set (UIBundleIntegrity only).
aruntyagiTutu
left a comment
There was a problem hiding this comment.
Thanks for the fix — mirroring the Layer-1 virtual-source skip (isVirtualSource/isDependency/isAsset) into the AST orphan check in runAstChecks makes sense, and the comment explaining why (bundler pseudo-sources like ?raw/webpack/vite internals aren't part of the submitted tree, with the ratio gate still catching abuse) is clear. Severity alignment (Critical→High for source-content-verification, matching the other Layer-1 rules) also seems reasonable.
One non-blocking gap: I don't see a new test that exercises this specific AST-path fix — e.g. a mapped node whose orig.source resolves to a virtual/dependency/asset path, asserting no "not present in the submitted source tree" (orphan) finding is raised. The existing virtual-source tests (does not flag virtual or relative sources, skips virtual and remote sources, `flags a virtual-source ratio above threshold") cover other validators/the Layer-1 byte-equal gate, but not this AST branch specifically. Since this was a real false-positive bug, a regression test would help guard against it recurring.
Not blocking — happy to approve once tests pass in CI.
- Normalize CRLF/CR to LF before byte-equal sourcesContent comparison so CRLF checkouts on Windows don't spuriously trip source-content-verification. - Normalize path.relative output to forward-slash when indexing source trees in source-content-verification, structural-coherence, and token-consistency so lookups against sourcemap sources[] entries succeed on Windows. - Fix structural-coherence whitespace-ratio denominator: track actual sample fires instead of dividing by floor(totalMappings/10). Previously the ratio could exceed 100% because sample count exceeded floor(N/10) for N not divisible by 10. - Replace vacuous Array.isArray / length>=0 assertions in validators-integration.test.ts with meaningful behavioral checks. - Remove MIGRATION.md — no sibling engine ships one and there is no precursor to migrate from now that this is the canonical location.
CI runs `tsc --build tsconfig.json && jest`. The tsc pass has been failing
across all platforms because:
- `encode()` expects `SourceMapSegment[][]` where each segment is a fixed-
length tuple (`[number, number, number, number]` etc.). Test helpers
declared their input as `number[][]` / `number[][][]`, which no longer
narrows to the tuple union in `@jridgewell/sourcemap-codec@1.5.5`.
- `new TraceMap({...})` inputs need to be typed as `SourceMapInput`
because the object literal's `mappings: string` field otherwise fails
to select the `EncodedSourceMapXInput` branch of the union.
Tighten the test helpers and cast the constructor inputs. Behavior
unchanged; jest was already green — this only fixes the pre-jest tsc gate.
aruntyagiTutu
left a comment
There was a problem hiding this comment.
Thanks for these fixes — all look correct:
- CRLF/CR normalization before byte-equal comparison and forward-slash normalization on
path.relativeoutput resolve real Windows correctness bugs (source indexing/lookup would otherwise fail on Windows checkouts). - The whitespace-ratio fix in
structural-coherence.tsis right —whitespaceSampleCountnow tracks actual sample fires (matching thesampleIndex % INTERVAL === 0condition) instead offloor(total/10), which could previously push the ratio over 100%. - The strengthened test assertions (replacing
Array.isArray(...)/length >= 0with real checks for byte-mismatch, unloadable-sourcemap, whitespace, and cross-file-jump findings) are a solid improvement — these now actually verify behavior instead of trivially passing. - Removing
MIGRATION.mdand fixing the tsc tuple-typing issues in test helpers are sensible cleanup.
LGTM.
| return [ | ||
| "packages/ENGINE-TEMPLATE" | ||
| "packages/ENGINE-TEMPLATE", | ||
| "packages/code-analyzer-uibundle-engine" |
There was a problem hiding this comment.
remember to revert this piece of code post PR merge
There was a problem hiding this comment.
I accidentally, removed this now. It seems to have broken the build. I will add it back and revert it post merge
Drop cross-repo and section-header comments so only WHY comments remain, keeping the validator source readable standalone.
The validate-changed-package-versions script had a temporary bypass for packages/code-analyzer-uibundle-engine while the package was unpublished.
…ource Drop section-header comments, inline what-comments, and jsdoc that only described obvious behavior. Well-named identifiers already carry the meaning.
aruntyagiTutu
left a comment
There was a problem hiding this comment.
Commit 299ceccc removes packages/code-analyzer-uibundle-engine from the unpublished-package whitelist in .node-scripts/validate-changed-package-versions.js, but the package still isn't published to npm (confirmed: npm view @salesforce/code-analyzer-uibundle-engine version → 404).
This will crash the verify-pr.yml version-check step for this PR itself (and any future PR touching this package) — not just fail a check, but throw an uncaught exception:
getLatestReleasedVersioncatches thenpm view404 and returnsundefined(handled fine).- But then
semver.parse(undefined)returnsnull(not a throw), and the subsequentsemver.lte(semver.parse(currentVersion), null)throwsInvalid version. Must be a string. Got type "object"— uncaught, crashing the script with a nonzero exit.
I verified this locally against the repo's actual semver dependency:
semver.parse(undefined) // => null
semver.lte(semver.parse('0.1.0'), null) // throws "Invalid version. Must be a string. Got type \"object\""
Since this same commit still touches files under packages/code-analyzer-uibundle-engine, the next CI run for this PR should hit this path. Recommend keeping the whitelist entry until the package is actually published (revert this piece of 299ceccc), or hardening identifyIncorrectlyVersionedPackages to treat an unresolvable releasedPackageVersion as "not yet published → skip" rather than assuming npm view failing implies a comparable prior version exists.
The rest of this push (comment trimming across the uibundle validators, dropping the // Babel columns are 0-based / // SFCA requires... type comments) is a reasonable readability cleanup with no functional change — no concerns there.
The uibundle package is not yet published to npm, so npm view returns 404 and the version-check crashes. Restore the whitelist entry until first publish.
Rule descriptions in messages.ts already document each rule's behavior; a separate README duplicates that content and drifts out of sync.
aruntyagiTutu
left a comment
There was a problem hiding this comment.
Confirmed — the whitelist entry for packages/code-analyzer-uibundle-engine is restored in 7b6b7f9e, resolving the version-check crash from my previous review. The uibundle README removal (e91cbdc7) is also reasonable since messages.ts already documents each rule's behavior and a separate README would drift out of sync.
The remaining files in this diff (apexguru-engine changes) came in via the merge from dev (PR #500), not new work on this branch — no action needed there.
LGTM.
aruntyagiTutu
left a comment
There was a problem hiding this comment.
Reviewed the new commit (f2aac4c) addressing the outstanding performance review blockers on this PR. Verified each fix directly:
Shared source-index build (engine.ts + sourcemap-io.ts). buildSourceIndex() is now extracted into sourcemap-io.ts and built once per target in analyzeSourceRules, then threaded through as an optional sourceIndex param to validateSourceContent, validateStructuralCoherence, and validateTokenConsistency (each falls back to building its own via options.sourceIndex ?? await buildSourceIndex(...), so existing unit tests that call the validators directly without a shared index still pass unmodified). This eliminates the 3x redundant walk()-based directory scans per rule selection that were flagged before — a real fix, not just a comment tweak.
Binary search in findNodeAtOffset. Replaced the linear scan with a binary search for the lower bound (byteOffset - tol), then a bounded scan of the tolerance window. The early-break-on-sorted-order assumption already existed in the prior linear implementation (if (n.byteOffset > byteOffset + tol) break), so this isn't a new correctness assumption — it's a legitimate algorithmic speedup over the same precondition (Babel's pre-order traversal producing non-decreasing byte offsets for the filtered significant-node list).
Coverage over-credit fix in coverage-analysis.ts. This is the most substantive fix: previously a single mapping at column 0 credited the entire line as mapped regardless of length, which meant a 5000-char minified line with one mapping read as ~100% covered. The new logic caps each mapping's reach at UNMAPPED_THRESHOLD (50 chars) and records the remainder as an unmapped gap. Traced the math by hand against the new test case (single mapping at col 0, 5000-char line): mappedChars becomes 50, coveragePct ~1%, correctly triggering the "excessive unmapped" signal that was previously silently missed. This is exactly the kind of correctness bug this validator exists to catch, so good catch and fix.
Tests. New regression tests cover both the coverage over-credit scenario and the AST orphan-source false-positive case for virtual/dependency/asset sources — both assert on concrete outcomes rather than shape, consistent with the team's testing standards.
Also confirmed the .node-scripts/validate-changed-package-versions.js whitelist entry for packages/code-analyzer-uibundle-engine (fixed in an earlier commit) is still intact, so CI's version-check crash risk remains resolved. No new issues found.
Automated re-review — post-
|
- Fix 0-based/1-based column convention across validators (columns now match SFCA convention) - Expand path-leakage roots to include /home, /root, /app, /build, /opt, /tmp, /var - Wire NoBundleTargetsFound/SkippedNoSourceTree/SkippedForTarget through message catalog - Surface malformed sourcemap JSON as SourcemapNotValidJson findings - Track and clean up tmp dirs in tests via installTmpDirCleanup() - Add error-path tests for malformed .js.map handling - Add TODO marker on uibundle-engine version-check whitelist entry - Return [...RULES] from describeRules to prevent external mutation - Rename ambiguous idx -> lastNameSegmentIdx in findAncestorNamed - Prefer contractions in user-facing catalog messages - Refactor repeated per-rule violation tests to it.each in engine.test.ts
aruntyagiTutu
left a comment
There was a problem hiding this comment.
Reviewed the latest commit (2147a6b) addressing follow-up feedback. Most of it is solid — the message-catalog wiring for NoBundleTargetsFound/SkippedNoSourceTree/SkippedForTarget, the SourcemapNotValidJson surfacing from collectSourceMaps's new parseErrors return, the installTmpDirCleanup() afterEach pattern, the it.each(perRuleViolationCases) test consolidation, describeRules returning [...RULES] defensively, the findAncestorNamed rename, the path-leakage root expansion, and the contraction fixes in messages.ts all look correct and well-executed.
One regression, though: the startColumn 0-based change is backwards. The commit message claims this now "matches SFCA convention," but SFCA's convention is 1-based, not 0-based:
packages/code-analyzer-regex-engine/src/engine.ts'sgetColumnNumberreturnscharIndex + 1for line 1 (1-based).packages/code-analyzer-eslint-engine/src/engine.ts'snormalizeStartValuedoesMath.max(startValue, 1)— enforces a 1-based floor.sfdx-code-analyzer-vscode/src/lib/diagnostics.ts'snormalizeLocationdefaults a missingstartColumnto1, andadjustToZeroBasedexplicitly subtracts 1 when converting aCodeLocationinto avscode.Position— i.e. the VS Code extension (the primary consumer ofCodeLocation) treats SFCA'sstartColumnas 1-based and does its own 0-based conversion for the editor.
So the four validators that were changed from startColumn: 1 (or region.startCol + 1) to startColumn: 0 (or region.startCol) in coverage-analysis.ts, invalid-source-references.ts, missing-sourcemap.ts, and path-leakage.ts are now emitting 0-based columns, which will render one character off in VS Code and is inconsistent with every other engine in the repo.
Notably, this wasn't caught by CI because the existing "1-based coordinates" assertion in validators.test.ts:239 (expect(v.codeLocations[0]!.startColumn).toBeGreaterThanOrEqual(1)) uses a fixture with a valid sourcemap and an existing source file, so it never exercises the specific finding types (SourceFileDoesNotExist, MissingSourcemapForFile, OrphanJsWithDangerousApi, PathLeakageFinding, or a real CoverageUnmappedRegion) that hit the now-broken 0-based paths.
Suggest reverting these four back to 1-based (region.startCol + 1 in coverage-analysis.ts, startColumn: 1 in the other three), and ideally strengthening the E2E test fixture to actually trigger at least one of these finding types so a future regression like this fails CI.
…tion - Emit 1-based startColumn from all validators (matches regex/eslint sibling engines) - Drop the +1 in engine.toViolation now that validators are 1-based directly - Add exact-column E2E test covering missing-sourcemap, path-leakage, invalid-source-references, coverage-analysis (guards against future off-by-one) - Strip remaining multi-line comment blocks from source and tests
aruntyagiTutu
left a comment
There was a problem hiding this comment.
Verified this commit against my prior CHANGES_REQUESTED on the startColumn convention.
On closer inspection, the previous behavior was already correct at the customer-facing level: engine.ts's toViolation() had Math.max(1, rawCol + 1), which converted the validators' 0-based ValidatorFinding.startColumn into a correctly 1-based CodeLocation.startColumn before it ever reached a consumer. My earlier flag compared the intermediate validator-level literal against SFCA's 1-based convention without tracing it through that conversion layer — that was my error, not a real bug in the prior commit.
That said, this commit is still a solid improvement:
- Moves 1-based emission directly into each validator (
coverage-analysis.ts,invalid-source-references.ts,missing-sourcemap.ts,path-leakage.ts,source-content-verification.ts) instead of relying on a central compensating+1intoViolation()— this matches how the regex/eslint engines emit literal 1-based values directly, which is more consistent and self-documenting. toViolation()now doesMath.max(1, rawCol), a straightforward pass-through-with-floor instead of an offset — removes a subtle "you must remember validators are 0-based" trap for future contributors.- New regression tests assert the exact value (
startColumn === 1) rather than just>= 1for missing-sourcemap, path-leakage, invalid-source-references, and coverage-analysis — meaningfully stronger than the previous loose bound, and would catch a future off-by-one in either direction.
Traced through each changed validator and confirmed the arithmetic is consistent: region.startCol + 1, literal 1, and node.column + 1 (Babel's 0-based loc.start.column) all now produce correct 1-based output given toViolation's updated pass-through.
No concerns with the rest of the diff (comment trimming in test-helpers.ts/engine.test.ts is a reasonable cleanup, not a loss of anything load-bearing).
Automated re-review — post-
|
Perf residual can be skipped for now, as it's from a generated file and the VlqDecodingFailed branch untested we will revisit later if needed |
|
Can you help me explain I believe only the source map is generated the source files are still hand written right ? |
ankitsinghkuntal09
left a comment
There was a problem hiding this comment.
Done W-23659201-ReSanity-8f0ed18-evidenceDoc: https://docs.google.com/document/d/1hvPWOkNMWY12X8uiXKyB5F93DgQ1UjCs/edit?usp=sharing&ouid=112790920304594839224&rtpof=true&sd=true
|
Approving this PR , there are some performance issues which will be addressed before the engine comes in as a default engine |
|
sf code-analyzer run --rule-selector all will invoke the engine we dont want that to avoid it |
…undles Coverage-analysis and token-consistency emitted noise on unmodified React+Vite bundles. Tightens both without weakening tamper detection: - coverage-analysis: raise per-region threshold on dense minified lines (>=5000 chars) to 2000 chars, cap per-file region emissions to the top 5 by size, and aggregate sub-threshold gaps toward the cumulative budget so many small gaps still surface. Line-1 banner discount applies to sub-threshold contributions symmetrically with regions. - token-consistency: suppress JSX-runtime synthetic names (jsx, jsxs, jsxDEV, Fragment) on .tsx/.jsx sources — structural, not tamper. Suppress namespace-prefix mismatches (Radix-style SelectPrimitive.X → SelectPrimitive at the container identifier). - source-content-verification: close line-1 blind spot in the dangerous-pattern filter (previously n.line > 1 dropped injections hidden in the single-line minified bundle past col 2000). Every suppression has a Warn-tier backstop: dangerous API-pattern scan past LINE1_BANNER_EXEMPT_CHARS, cumulative-budget aggregation, and structural-coherence bounds checks still fire on all planted tampers. Local rescan: app-one clean 120 -> 5 findings, app-two tampered 148 -> 26 with all 18 High/Moderate tamper findings preserved.
One option that we are looking at is adding a similar UIBundle check in the same place, to restrict it |
yes this makes sense |
The uibundle engine is scoped to bundle-integrity scans and should not run as part of a general `sf code-analyzer run --rule-selector all`. Mirrors the DevPreviewApexGuru opt-in pattern in packages/code-analyzer-core/src/rules.ts. - Stamp a `UIBundle` tag on every uibundle rule alongside UIBundleIntegrity. - In Rule.matchesRuleSelector, extend the opt-in branch to also fire when tags include 'uibundle' — such rules are only selectable by engine name, rule name, or explicit tag; excluded from 'all' and severity selectors. - Add UIBundleEnginePlugin stub and describe block asserting: NOT selected by all/severity name/severity number; IS selected by engine name, rule name, UIBundle tag, and UIBundleIntegrity tag. - Update the uibundle engine goldfile to reflect the new tag.
Yes, the map is generated, and the source could grow over time. We’d definitely like to revisit the performance aspect once the initial changes are in and we’ve reached a functionally ready state. |
Automated re-review — post-
|
| const findings: ValidatorFinding[] = []; | ||
|
|
||
| await walk(distPath, async (jsPath) => { | ||
| if (!jsPath.endsWith(".js")) return; |
There was a problem hiding this comment.
@amritmishra-sf @nikhil-mittal-165 Here and in other such checks in other files should .mjs/.cjs files also be included?
There was a problem hiding this comment.
Hi @jag-j, we’re trying to use this tool to perform integrity checks for UI Bundles, which include a dist directory in the packaged code that gets installed in the subscriber org. So yes, these files are important for our use case at the moment.
There was a problem hiding this comment.
@amritmishra-sf Are you saying you will make the change to include cjs, mjs files?
There was a problem hiding this comment.
yes, ideally these should be. I think it was missed in the initial changes. Thanks @jag-j for pointing that out. I will push the changes for those two as well
…idators
Every validator hard-coded `.endsWith(".js")` for the compiled file and
`.endsWith(".js.map")` for the sourcemap, so bundlers configured for
ESM output (Vite `--format es` -> main.mjs / main.mjs.map) or CJS
output (main.cjs / main.cjs.map) were silently skipped. An attacker
could ship a tampered .mjs or .cjs bundle and bypass all six tamper
detectors.
- Centralize the extension check in classification.ts as `isCompiledJs`
and `isSourcemap`, each accepting .js/.mjs/.cjs (plus their .map
counterparts). Bundler map convention `<compiled-filename>.map` is
preserved for all three, so `${jsPath}.map` derivations remain correct.
- Rewire the five validators (missing-sourcemap, coverage-analysis,
source-content-verification, structural-coherence, token-consistency)
and the sourcemap walker (sourcemap-io) to use the helpers.
- Add parametrized regression tests: missing-sourcemap fires on a
.mjs and .cjs bundle with no map; path-leakage fires on a .mjs and
.cjs bundle with a leaked absolute path. Classification unit tests
cover the new helpers directly.
… 5%/2000-char constants
Fixes doc/test drift introduced when the coverage-analysis thresholds were bumped
from 2%/150-char to 5%/2000-char. The customer-facing rule description still cited
the old numbers, contradicting the finding message ("Threshold is 5%"). Two tests
had stale labels or fixtures too small to exercise the cumulative-budget path,
making the assertion pass vacuously.
- messages.ts + goldfile: '2% / 150 chars' -> '5% / 2000 chars'
- validators.test.ts: rename 'exempts up to 150 chars' -> 'up to 2000 chars'
- validators-integration.test.ts: bump line-1 fixture from 200 to 3000 chars and
assert on the 'Excessive cumulative unmapped' message specifically so the test
now enforces its stated intent
…lidators
An independent adversarial review turned up three false-negative holes an attacker
could use to silence all 8 uibundle rules on a tampered bundle. Each fix closes a
distinct class of bypass and adds adversarial tests.
1. missing-sourcemap accepted //# sourceMappingURL= comment presence alone as proof
of a sourcemap. Attacker deletes the .map file, keeps the comment; every other
validator silently returns on ENOENT and the whole engine goes quiet. hasSourcemap
now (a) requires the colocated .map to be a regular file (rejects directories and
the isFile-false symlinks walk() would skip anyway), (b) rejects data: and remote
scheme:// URLs (no downstream validator decodes them), (c) resolves the URL
relative to the .js and requires the target to be inside distPath, satisfy
isSourcemap(), and be a regular file - so anything downstream walkers wouldn't
scan is now flagged.
2. vlq-integrity still used endsWith(".js.map") after the .mjs/.cjs migration; every
other validator was rewired to isSourcemap() in 1807c2f. Switch this last one so
fabricated VLQ in an ESM (.mjs) or CJS (.cjs) bundle no longer evades range checks.
3. isVirtualSource classified any source path containing '?' as virtual, letting
source-content-verification and token-consistency skip byte-equal, AST-type,
and dangerous-unmapped checks. Attacker points injected code at
src/injected.js?x. Restrict to concrete Vite/webpack/Rollup query forms
(?vue, ?url, ?raw, ?worker, ?commonjs-proxy, ?lang.ts, etc.) with fixed values
rather than any k=v.
Tests: 103/103 pass. Adversarial cases added for each bypass class (nonexistent
.map, data URL, remote URL, wrong-extension target, self-reference, colocated
directory, outside-distPath URL, .mjs.map/.cjs.map VLQ scan, ?x=1 / ?tampered=1
attacker suffixes).
Automated re-review — post-
|
…Sourcemap The colocated and resolved-URL branches used fs.stat, which follows symlinks, while every enumerator in the engine (collectJsFiles, sourcemap-io walk) uses Dirent.isFile() which is false for symlinks. A symlinked .js.map was therefore accepted as "present" by missing-sourcemap but silently skipped by vlq-integrity, path-leakage, and invalid-source-references. Not a practical bypass (requires an attacker who already controls dist/) but an inconsistent walker policy is a code-smell in a tamper detector. Switch both stat calls to lstat so the rule matches the walker policy — a symlinked map is treated as absent and flagged. Adds one regression test.
✅ Symlink fix (
|
…kflow Adds a workflow_dispatch input toggle and validation branch for the new code-analyzer-uibundle-engine package so it can be selected for release alongside the other engine packages. Mirrors the pattern used for code-analyzer-apexguru-engine (PR #451).


Summary
Adds a new SFCA v5 engine plugin
@salesforce/code-analyzer-uibundle-enginethat validates UI Bundle build output.The engine is named generically (
uibundle) so additional UI-bundle rule families can be added later without a package rename. The initial ruleset ships 8 sourcemap-integrity rules.What's included
packages/code-analyzer-uibundle-engine/— engine classUIBundleEngine(NAME =uibundle), pluginUIBundleEnginePlugin, following the sibling-engine layout.missing-sourcemap,path-leakage,invalid-source-references,vlq-integrity,source-content-verification(Critical),coverage-analysis,structural-coherence,token-consistency.getMessageFromCatalog, goldfile-tested attest/test-data/uibundle-engine-goldfile.json..node-scripts/validate-changed-package-versions.jsfor the not-yet-published package.Test plan
npm run build— cleannpm run lint— cleannpx jest --coverage— 69/69 pass, 4 suitesCompanion PR
CLI-side registration: forcedotcom/code-analyzer#2080 — pins
@salesforce/code-analyzer-uibundle-engine@0.1.0-SNAPSHOT, so it can only go green once this engine is published. Sequence the merge accordingly.Related