-
Notifications
You must be signed in to change notification settings - Fork 35
⭐ Add Firebase provider for security scanning #7128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
philipbalinov
wants to merge
6
commits into
main
Choose a base branch
from
feat/firebase-provider
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
40c3c83
✨ Add Firebase provider for security scanning
philipbalinov 420cc47
fix: address PR review feedback for firebase provider
philipbalinov d49f2e3
fix: use init functions for child resources to fix gRPC serialization
philipbalinov 42d7a51
✨ Add email enumeration, source map, and Firestore checks
philipbalinov 9ea4567
✨ Add listCollectionIds probe to Firestore check
philipbalinov 70bb92d
fix: address PR review feedback for firebase provider
philipbalinov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -205,6 +205,7 @@ PROVIDERS := \ | |
| cloudformation \ | ||
| depsdev \ | ||
| equinix \ | ||
| firebase \ | ||
| gcp \ | ||
| github \ | ||
| gitlab \ | ||
|
|
||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| // Copyright Mondoo, Inc. 2024, 2026 | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
|
|
||
| package config | ||
|
|
||
| import ( | ||
| "go.mondoo.com/mql/v13/providers-sdk/v1/inventory" | ||
| "go.mondoo.com/mql/v13/providers-sdk/v1/plugin" | ||
| "go.mondoo.com/mql/v13/providers/firebase/provider" | ||
| ) | ||
|
|
||
| var Config = plugin.Provider{ | ||
| Name: "firebase", | ||
| ID: "go.mondoo.com/mql/v13/providers/firebase", | ||
| Version: "13.0.0", | ||
| ConnectionTypes: []string{provider.DefaultConnectionType}, | ||
| Connectors: []plugin.Connector{ | ||
| { | ||
| Name: "firebase", | ||
| Use: "firebase [domain]", | ||
| Short: "a Firebase project", | ||
| Long: `Use the firebase provider to check Firebase projects for security misconfigurations via public endpoints. | ||
|
|
||
| Examples: | ||
| mql shell firebase --project-id my-project --api-key AIzaSy... | ||
| mql shell firebase --domain myapp.firebaseapp.com | ||
| mql shell firebase myapp.web.app | ||
| mql run firebase --project-id my-project -c "firebase.project.realtimeDatabase { publiclyReadable }" | ||
|
|
||
| Notes: | ||
| - Provide --project-id and --api-key for direct checks, or --domain to auto-discover them. | ||
| - A positional argument is treated as a domain. | ||
| - All checks are read-only and use only public HTTP endpoints. | ||
| `, | ||
| MinArgs: 0, | ||
| MaxArgs: 1, | ||
| Flags: []plugin.Flag{ | ||
| { | ||
| Long: "project-id", | ||
| Type: plugin.FlagType_String, | ||
| Default: "", | ||
| Desc: "Firebase project ID", | ||
| }, | ||
| { | ||
| Long: "api-key", | ||
| Type: plugin.FlagType_String, | ||
| Default: "", | ||
| Desc: "Firebase API key (web API key)", | ||
| }, | ||
| { | ||
| Long: "domain", | ||
| Type: plugin.FlagType_String, | ||
| Default: "", | ||
| Desc: "Domain to scan for Firebase configuration", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| AssetUrlTrees: []*inventory.AssetUrlBranch{ | ||
| { | ||
| PathSegments: []string{"technology=saas", "category=firebase"}, | ||
| Key: "kind", | ||
| Title: "Kind", | ||
| Values: map[string]*inventory.AssetUrlBranch{ | ||
| "project": nil, | ||
| }, | ||
| }, | ||
| }, | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,257 @@ | ||
| // Copyright Mondoo, Inc. 2024, 2026 | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
|
|
||
| package connection | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "regexp" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/cockroachdb/errors" | ||
| "github.com/rs/zerolog/log" | ||
| "go.mondoo.com/mql/v13/providers-sdk/v1/inventory" | ||
| "go.mondoo.com/mql/v13/providers-sdk/v1/plugin" | ||
| ) | ||
|
|
||
| type FirebaseConnection struct { | ||
| plugin.Connection | ||
| Conf *inventory.Config | ||
| asset *inventory.Asset | ||
| projectId string | ||
| apiKey string | ||
| authDomain string | ||
| domain string | ||
| httpClient *http.Client | ||
| } | ||
|
|
||
| func NewFirebaseConnection(id uint32, asset *inventory.Asset, conf *inventory.Config) (*FirebaseConnection, error) { | ||
| conn := &FirebaseConnection{ | ||
| Connection: plugin.NewConnection(id, asset), | ||
| Conf: conf, | ||
| asset: asset, | ||
| httpClient: &http.Client{Timeout: 15 * time.Second}, | ||
| } | ||
|
|
||
| if conf.Options != nil { | ||
| conn.projectId = conf.Options["project-id"] | ||
| conn.apiKey = conf.Options["api-key"] | ||
| conn.domain = conf.Options["domain"] | ||
| } | ||
|
|
||
| // If we have a domain but no project ID, try to resolve Firebase config from the domain | ||
| if conn.domain != "" && conn.projectId == "" { | ||
| if err := conn.resolveFromDomain(); err != nil { | ||
| return nil, errors.Newf("could not discover Firebase config from domain %q: %v", conn.domain, err) | ||
| } | ||
| } | ||
|
|
||
| if conn.projectId == "" && conn.domain == "" { | ||
| return nil, errors.New("either --project-id or --domain must be provided") | ||
| } | ||
|
|
||
| // Derive authDomain if not set | ||
| if conn.authDomain == "" && conn.projectId != "" { | ||
| conn.authDomain = conn.projectId + ".firebaseapp.com" | ||
| } | ||
|
|
||
| return conn, nil | ||
| } | ||
|
|
||
| // resolveFromDomain attempts to discover Firebase project configuration from a domain. | ||
| // It tries multiple strategies: | ||
| // 1. Firebase Hosting /__/firebase/init.js (only works for Firebase-hosted sites) | ||
| // 2. Inline HTML/JS scraping of the main page | ||
| // 3. Fetching linked JavaScript bundles and scanning them for Firebase config | ||
| func (c *FirebaseConnection) resolveFromDomain() error { | ||
| domain := c.domain | ||
| if !strings.Contains(domain, "://") { | ||
| domain = "https://" + domain | ||
| } | ||
| domain = strings.TrimRight(domain, "/") | ||
|
|
||
| // Strategy 1: Try /__/firebase/init.js (Firebase Hosting auto-serves this) | ||
| initURL := domain + "/__/firebase/init.js" | ||
| log.Debug().Str("url", initURL).Msg("trying Firebase Hosting init.js") | ||
| if err := c.extractConfigFromBody(initURL); err == nil && c.projectId != "" { | ||
| log.Info().Str("projectId", c.projectId).Msg("resolved Firebase config from init.js") | ||
| return nil | ||
| } | ||
|
|
||
| // Strategy 2: Fetch main page HTML, scan inline scripts and discover JS bundle URLs | ||
| log.Debug().Str("url", domain).Msg("fetching main page for Firebase config discovery") | ||
| resp, err := c.httpClient.Get(domain) | ||
| if err != nil { | ||
| return errors.Wrap(err, "failed to fetch domain") | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| return fmt.Errorf("HTTP %d from %s", resp.StatusCode, domain) | ||
| } | ||
|
|
||
| body, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20)) // 2MB limit for HTML | ||
| if err != nil { | ||
| return errors.Wrap(err, "failed to read page body") | ||
| } | ||
|
|
||
| html := string(body) | ||
|
|
||
| // Check inline scripts in the HTML for Firebase config | ||
| c.extractConfigFromContent(html) | ||
| if c.projectId != "" { | ||
| log.Info().Str("projectId", c.projectId).Msg("resolved Firebase config from inline HTML") | ||
| return nil | ||
| } | ||
|
|
||
| // Strategy 3: Find linked JS files and scan them for Firebase config. | ||
| // Apps using Firebase Auth typically bundle the config in their JS. | ||
| scriptURLs := extractScriptSrcs(html, domain) | ||
| log.Debug().Int("count", len(scriptURLs)).Msg("found linked script URLs to scan") | ||
|
|
||
| for _, scriptURL := range scriptURLs { | ||
| log.Debug().Str("url", scriptURL).Msg("scanning JS bundle for Firebase config") | ||
| if err := c.extractConfigFromBody(scriptURL); err != nil { | ||
| log.Debug().Err(err).Str("url", scriptURL).Msg("failed to fetch JS bundle") | ||
| continue | ||
| } | ||
| if c.projectId != "" { | ||
| log.Info().Str("projectId", c.projectId).Str("source", scriptURL).Msg("resolved Firebase config from JS bundle") | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| if c.projectId == "" { | ||
| return errors.New("could not find Firebase configuration (projectId/apiKey) in domain content or linked scripts") | ||
| } | ||
|
|
||
|
mondoo-code-review[bot] marked this conversation as resolved.
|
||
| return nil | ||
| } | ||
|
|
||
| var ( | ||
| // Match Firebase config patterns in JS — handles both quoted keys and unquoted keys, | ||
| // property assignment and object literal styles | ||
| reProjectId = regexp.MustCompile(`(?:["']?projectId["']?\s*[:=]\s*["']|FIREBASE_PROJECT_ID["']?\s*[:=]\s*["'])([a-zA-Z0-9_-]+)["']`) | ||
| reApiKey = regexp.MustCompile(`(?:["']?apiKey["']?\s*[:=]\s*["']|FIREBASE_API_KEY["']?\s*[:=]\s*["'])(AIza[a-zA-Z0-9_-]+)["']`) | ||
| reAuthDomain = regexp.MustCompile(`(?:["']?authDomain["']?\s*[:=]\s*["']|FIREBASE_AUTH_DOMAIN["']?\s*[:=]\s*["'])([a-zA-Z0-9._-]+)["']`) | ||
|
|
||
| // Match <script src="..."> tags in HTML | ||
| reScriptSrc = regexp.MustCompile(`<script[^>]+src=["']([^"']+)["']`) | ||
| ) | ||
|
|
||
| // extractConfigFromBody fetches a URL and scans its body for Firebase config. | ||
| func (c *FirebaseConnection) extractConfigFromBody(url string) error { | ||
| resp, err := c.httpClient.Get(url) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| return fmt.Errorf("HTTP %d from %s", resp.StatusCode, url) | ||
| } | ||
|
|
||
|
mondoo-code-review[bot] marked this conversation as resolved.
|
||
| body, err := io.ReadAll(io.LimitReader(resp.Body, 5<<20)) // 5MB limit for JS bundles | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| c.extractConfigFromContent(string(body)) | ||
| return nil | ||
| } | ||
|
|
||
| // extractConfigFromContent scans text content for Firebase config patterns. | ||
| func (c *FirebaseConnection) extractConfigFromContent(content string) { | ||
| if m := reProjectId.FindStringSubmatch(content); len(m) > 1 && c.projectId == "" { | ||
| c.projectId = m[1] | ||
| } | ||
| if m := reApiKey.FindStringSubmatch(content); len(m) > 1 && c.apiKey == "" { | ||
| c.apiKey = m[1] | ||
| } | ||
| if m := reAuthDomain.FindStringSubmatch(content); len(m) > 1 && c.authDomain == "" { | ||
| c.authDomain = m[1] | ||
| } | ||
| } | ||
|
|
||
| // extractScriptSrcs finds all <script src="..."> URLs in HTML, resolving relative paths. | ||
| func extractScriptSrcs(html string, baseURL string) []string { | ||
| matches := reScriptSrc.FindAllStringSubmatch(html, -1) | ||
| var urls []string | ||
| seen := map[string]bool{} | ||
|
|
||
| for _, m := range matches { | ||
| src := m[1] | ||
|
|
||
| // Resolve relative URLs | ||
| if strings.HasPrefix(src, "//") { | ||
| src = "https:" + src | ||
| } else if strings.HasPrefix(src, "/") { | ||
| src = baseURL + src | ||
| } else if !strings.HasPrefix(src, "http") { | ||
| src = baseURL + "/" + src | ||
| } | ||
|
|
||
| // Skip external CDNs that won't have app-specific Firebase config | ||
| if strings.Contains(src, "googleapis.com") || | ||
| strings.Contains(src, "gstatic.com") || | ||
| strings.Contains(src, "google-analytics.com") || | ||
| strings.Contains(src, "googletagmanager.com") { | ||
| continue | ||
| } | ||
|
|
||
| if !seen[src] { | ||
| seen[src] = true | ||
| urls = append(urls, src) | ||
| } | ||
| } | ||
|
|
||
| return urls | ||
| } | ||
|
|
||
| func (c *FirebaseConnection) Name() string { | ||
| return "firebase" | ||
| } | ||
|
|
||
| func (c *FirebaseConnection) Asset() *inventory.Asset { | ||
| return c.asset | ||
| } | ||
|
|
||
| func (c *FirebaseConnection) ProjectId() string { | ||
| return c.projectId | ||
| } | ||
|
|
||
| func (c *FirebaseConnection) ApiKey() string { | ||
| return c.apiKey | ||
| } | ||
|
|
||
| func (c *FirebaseConnection) AuthDomain() string { | ||
| return c.authDomain | ||
| } | ||
|
|
||
| func (c *FirebaseConnection) Domain() string { | ||
| return c.domain | ||
| } | ||
|
|
||
| func (c *FirebaseConnection) HttpClient() *http.Client { | ||
| return c.httpClient | ||
| } | ||
|
|
||
| func (c *FirebaseConnection) PlatformInfo() *inventory.Platform { | ||
| return &inventory.Platform{ | ||
| Name: "firebase-project", | ||
| Family: []string{"firebase"}, | ||
| Kind: "api", | ||
| Title: "Firebase Project", | ||
| } | ||
| } | ||
|
|
||
| func (c *FirebaseConnection) Identifier() string { | ||
| id := c.projectId | ||
| if id == "" { | ||
| id = c.domain | ||
| } | ||
| return "//platformid.api.mondoo.app/runtime/firebase/project/" + id | ||
| } | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| // Copyright Mondoo, Inc. 2024, 2026 | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "go.mondoo.com/mql/v13/providers-sdk/v1/plugin/gen" | ||
| "go.mondoo.com/mql/v13/providers/firebase/config" | ||
| ) | ||
|
|
||
| func main() { | ||
| gen.CLI(&config.Config) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.