feat(browserext): add browser extension inventory scan - #192
Open
raysubham wants to merge 5 commits into
Open
Conversation
Inventories installed browser extensions across Chrome, Edge, Brave, Chromium and Firefox, reporting per-browser coverage alongside the findings so the backend can tell an empty browser from an unreadable one. Coverage is membership-complete: only scanned and partial browsers carry an authoritative extension list, while failed and not_present ship none. Chromium profiles are read from Local State plus Secure Preferences and Preferences; enabled state comes from the disable_reasons bitmask. Gecko profiles come from profiles.ini and extensions.json, carrying the signature state instead of the store fields. Extension name and locale are attribute reads, so a missing or unreadable manifest reduces the finding and marks the browser partial rather than discarding it. Adds a no-follow mode to safepath: an openat chain with O_NOFOLLOW on unix, and an unfollowed open verified against the kernel's final path on Windows. All reads are byte-capped and the phase carries its own deadline. Per-browser and per-profile caps bound one browser; the total finding cap and the deadline end the payload. Behind the browser_extensions_scan feature gate, off by default.
Restricting an extension's site access does not rewrite its up-front grant. The browser sets withholding_permissions and moves the hosts that survive into the runtime record, leaving the withheld ones in granted_permissions as a record of what was originally handed over. Unioning the two records therefore reported access the user had taken back, so a restricted extension and an unrestricted one produced identical host lists. Hosts now come from the runtime record alone once the flag is set. API permissions are never withheld, so that union is unchanged, and a browser that does not write the flag keeps today's behaviour.
…ked paths Chromium collapsed granted_permissions.explicit_host and scriptable_host into one list, so an extension that can inject code into a host looked the same as one that can only reach it with a request. Split them: scriptable_host_permissions is derived from the already-capped host union, so it is a subset by construction. The field is a pointer, since an empty list means the extension injects nowhere and an absent one means the engine does not record the distinction. Manifest version now ships on both engines. A version 2 extension can hold blocking request interception, which version 3 removed, so the two are not the same risk class. Unpacked extensions no longer degrade a browser to partial for ever. The refusal to resolve an absolute path stays, but nothing was read and failed, so there is nothing to report as degraded. Their load path ships as install_path, capped and omitted rather than shortened, because an unreviewed extension running out of a user directory is the signal and a blank name is not. Gecko read runtime grants from nowhere: Firefox writes them only to extension-preferences.json, which was never opened. Union it into each add-on already present in extensions.json, dropping internal: bookkeeping and routing host patterns to origins. The file can only add attributes, so a missing one is silence while an unparseable or refused one degrades. Data collection comes across with it, keeping ["none"] distinct from nothing declared. Also carries the Chromium disable_reasons actor map, which reads the reason bits rather than the aggregate state so a policy-disabled extension is not reported as a user choice.
The shared fixture carried scriptable hosts, manifest version, data collection and install path, but nothing checked them, so a regression that stopped emitting one would have passed here and only shown up as a missing column. Coverage now requires the fixture to keep exercising all three scriptable states, since an empty list and an absent one are different answers, both manifest versions plus an unrecorded one, and a declared data collection. The invariants a reader rejects over are checked alongside them: a scriptable host absent from host_permissions, a scriptable list on a gecko browser, and an install path on anything but an unpacked extension.
An extension installed in several profiles reduces to one finding, and the whole record was taken from the profile whose path sorted first. A profile with site access withheld could therefore supply the permission lists while another profile held broad access, so the finding under-reported what the machine can reach. Rank occurrences instead: enabled state first, then broad host access, broad scriptable access, host count, permission count, and the profile path last as a total tiebreak so repeat scans stay identical. Ranking by state first makes the winning record's own state equal the state the row reports, so version, store and permissions always describe a profile the row really is in. Broad host access uses the same rule the reader applies, including the http and https pattern pair, so both sides agree on what breadth means.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Inventories installed browser extensions across Chrome, Edge, Brave, Chromium and Firefox, and reports per-browser coverage alongside the findings.
Coverage answers whether the reported set of extension identities is complete for that browser:
scannedpartialfailednot_presentHow
Local State, extensions fromSecure PreferencesthenPreferences. Enabled state derives from thedisable_reasonsbitmask, which also gives the reason an extension is off (user, browser, policy).profiles.ini, extensions fromextensions.json, carrying the signature state instead of the store listing fields.manifest.jsonand_locales/<loc>/messages.jsonare attribute reads. If one is unreadable the finding is kept without its name and the browser degrades topartial; only a failed membership document fails the browser.openatchain withO_NOFOLLOWper component on unix; on Windows a single unfollowed open whose final path is verified against the resolved path. Windows therefore catches an ancestor junction after traversal rather than before, which is a stated ceiling shared with the existing credential scan.Testing
gofmt,go vet,go mod tidy(no drift),golangci-lint(0 issues),go test -race ./...(45/45),make smoke(45/45), andCGO_ENABLED=0cross-compiles for windows/amd64 and linux/amd64 all pass.gosecreports only pre-existing findings; none are in the added or changed lines.A golden payload under
internal/model/testdata/pins the wire shape: it round-trips withDisallowUnknownFields, exercises every enum value a single valid payload can carry, and asserts the coverage invariants a reader rejects the block over.Validated on a macOS laptop with no Full Disk Access: Chrome and Brave detected, delisted rows correct, no prompts.
Rollout
Behind the
browser_extensions_scanfeature gate, off by default. The backend half lands separately and must deploy first.