|
| 1 | +from bbot.modules.templates.gitlab import GitLabBaseModule |
| 2 | + |
| 3 | + |
| 4 | +class gitlab_onprem(GitLabBaseModule): |
| 5 | + watched_events = ["HTTP_RESPONSE", "TECHNOLOGY", "SOCIAL"] |
| 6 | + produced_events = [ |
| 7 | + "TECHNOLOGY", |
| 8 | + "SOCIAL", |
| 9 | + "CODE_REPOSITORY", |
| 10 | + "FINDING", |
| 11 | + ] |
| 12 | + flags = ["active", "safe", "code-enum"] |
| 13 | + meta = { |
| 14 | + "description": "Detect self-hosted GitLab instances and query them for repositories", |
| 15 | + "created_date": "2024-03-11", |
| 16 | + "author": "@TheTechromancer", |
| 17 | + } |
| 18 | + |
| 19 | + # Optional GitLab access token (only required for gitlab.com, but still |
| 20 | + # supported for on-prem installations that expose private projects). |
| 21 | + options = {"api_key": ""} |
| 22 | + options_desc = {"api_key": "GitLab access token (for self-hosted instances only)"} |
| 23 | + |
| 24 | + # Allow accepting events slightly beyond configured max distance so we can |
| 25 | + # discover repos on neighbouring infrastructure. |
| 26 | + scope_distance_modifier = 2 |
| 27 | + |
| 28 | + async def handle_event(self, event): |
| 29 | + if event.type == "HTTP_RESPONSE": |
| 30 | + await self.handle_http_response(event) |
| 31 | + elif event.type == "TECHNOLOGY": |
| 32 | + await self.handle_technology(event) |
| 33 | + elif event.type == "SOCIAL": |
| 34 | + await self.handle_social(event) |
| 35 | + |
| 36 | + async def filter_event(self, event): |
| 37 | + # only accept out-of-scope SOCIAL events |
| 38 | + if event.type == "HTTP_RESPONSE": |
| 39 | + if event.scope_distance > self.scan.scope_search_distance: |
| 40 | + return False, "event is out of scope distance" |
| 41 | + elif event.type == "TECHNOLOGY": |
| 42 | + if not event.data["technology"].lower().startswith("gitlab"): |
| 43 | + return False, "technology is not gitlab" |
| 44 | + if not self.helpers.is_ip(event.host) and self.helpers.tldextract(event.host).domain == "gitlab": |
| 45 | + return False, "gitlab instance is not self-hosted" |
| 46 | + elif event.type == "SOCIAL": |
| 47 | + if event.data["platform"] != "gitlab": |
| 48 | + return False, "platform is not gitlab" |
| 49 | + _, domain = self.helpers.split_domain(event.host) |
| 50 | + if domain in self.saas_domains: |
| 51 | + return False, "gitlab instance is not self-hosted" |
| 52 | + return True |
| 53 | + |
| 54 | + async def handle_http_response(self, event): |
| 55 | + """Identify GitLab servers from HTTP responses.""" |
| 56 | + headers = event.data.get("header", {}) |
| 57 | + if "x_gitlab_meta" in headers: |
| 58 | + url = event.parsed_url._replace(path="/").geturl() |
| 59 | + await self.emit_event( |
| 60 | + {"host": str(event.host), "technology": "GitLab", "url": url}, |
| 61 | + "TECHNOLOGY", |
| 62 | + parent=event, |
| 63 | + context=f"{{module}} detected {{event.type}}: GitLab at {url}", |
| 64 | + ) |
| 65 | + description = f"GitLab server at {event.host}" |
| 66 | + await self.emit_event( |
| 67 | + {"host": str(event.host), "description": description}, |
| 68 | + "FINDING", |
| 69 | + parent=event, |
| 70 | + context=f"{{module}} detected {{event.type}}: {description}", |
| 71 | + ) |
| 72 | + |
| 73 | + async def handle_technology(self, event): |
| 74 | + """Enumerate projects & groups once we know a host is GitLab.""" |
| 75 | + base_url = self.get_base_url(event) |
| 76 | + |
| 77 | + # Projects owned by the authenticated user (or public projects if no |
| 78 | + # authentication). |
| 79 | + projects_url = self.helpers.urljoin(base_url, "api/v4/projects?simple=true") |
| 80 | + await self.handle_projects_url(projects_url, event) |
| 81 | + |
| 82 | + # Group enumeration. |
| 83 | + groups_url = self.helpers.urljoin(base_url, "api/v4/groups?simple=true") |
| 84 | + await self.handle_groups_url(groups_url, event) |
0 commit comments