Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions internal/sbom/chrome/preferences.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package chrome

import (
"encoding/json"
"log"
"os"
"path/filepath"
"strings"
)

// Chrome persists extension state in two JSON files at the root of each profile:
// - Preferences: regular user-installed extensions
// - Secure Preferences: policy/force-installed extensions
type extensionSetting struct {
State *int `json:"state"`
DisableReasons []int `json:"disable_reasons"`
}

type preferencesFile struct {
Extensions struct {
Settings map[string]extensionSetting `json:"settings"`
} `json:"extensions"`
}

func isValidChromeProfileDir(profileDir string) bool {
cleanDir := filepath.Clean(profileDir)
name := filepath.Base(cleanDir)
if name != "Default" && !strings.HasPrefix(name, "Profile ") {
return false
}

parent := filepath.Base(filepath.Dir(cleanDir))
return parent != "." && parent != string(filepath.Separator)
}

// readProfileExtensionStates returns a map from extension ID to true (enabled)
// or false (disabled) for extensions explicitly listed in the profile's
// Preferences or Secure Preferences. Secure Preferences wins on conflict.
func readProfileExtensionStates(profileDir string) map[string]bool {
states := map[string]bool{}
if !isValidChromeProfileDir(profileDir) {
log.Printf("Skipping Chrome preference scan for unexpected profile directory: %s", profileDir)
return states
}

for _, name := range []string{"Preferences", "Secure Preferences"} {
path := filepath.Join(filepath.Clean(profileDir), name)
data, err := os.ReadFile(path)
if err != nil {
continue
}

var prefs preferencesFile
if err := json.Unmarshal(data, &prefs); err != nil {
log.Printf("Failed to parse %s at %s: %v", name, profileDir, err)
continue
}

for id, setting := range prefs.Extensions.Settings {
if len(setting.DisableReasons) > 0 {
states[id] = false
continue
}
if setting.State != nil {
states[id] = *setting.State == 1
}
}
}

return states
}
25 changes: 24 additions & 1 deletion internal/sbom/chrome/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,30 @@ func (c *ChromeExtensions) Installations(ctx context.Context) ([]sbom.InstalledV
// SBOM scans all profiles within the browser data directory (DataPath)
// and reports every installed version of each extension, deduplicating
// identical (id, version) pairs that appear across multiple profiles.
//
// Each package carries a State of "enabled" or "disabled" reflecting the
// per-profile Preferences / Secure Preferences metadata. An extension enabled
// in any profile where it is installed is reported as enabled. Missing state
// entries still fail open to enabled so that extensions on disk but unknown to
// Chrome's metadata continue to surface in the SBOM.
func (c *ChromeExtensions) SBOM(_ context.Context, installation sbom.InstalledVersion) ([]sbom.Package, error) {
profiles := findProfilesWithExtensions(installation.DataPath)

seen := make(map[string]bool)
enabledSomewhere := make(map[string]bool)
var packages []sbom.Package

for _, profile := range profiles {
extDir := filepath.Join(installation.DataPath, profile, "Extensions")
profileDir := filepath.Join(installation.DataPath, profile)
extDir := filepath.Join(profileDir, "Extensions")
entries, err := os.ReadDir(extDir)
if err != nil {
log.Printf("Failed to read extensions directory for profile %s: %v", profile, err)
continue
}

states := readProfileExtensionStates(profileDir)

for _, entry := range entries {
if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
continue
Expand All @@ -49,6 +59,11 @@ func (c *ChromeExtensions) SBOM(_ context.Context, installation sbom.InstalledVe
extensionID := entry.Name()
extPath := filepath.Join(extDir, extensionID)

enabled, known := states[extensionID]
if !known || enabled {
enabledSomewhere[extensionID] = true
}

for _, pkg := range readExtensionVersions(extPath, extensionID) {
key := pkg.Id + "@" + pkg.Version
if seen[key] {
Expand All @@ -60,5 +75,13 @@ func (c *ChromeExtensions) SBOM(_ context.Context, installation sbom.InstalledVe
}
}

for i := range packages {
if enabledSomewhere[packages[i].Id] {
packages[i].State = "enabled"
} else {
packages[i].State = "disabled"
}
}

return packages, nil
}
Loading
Loading