gitlabrat 1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
labrat/core/agent.py ADDED
@@ -0,0 +1,120 @@
1
+ from urllib.parse import urlparse
2
+ import requests
3
+ from bs4 import BeautifulSoup
4
+ import gitlab
5
+
6
+ class Agent:
7
+ def __init__(self, url, use_ldap=False, username=None, password=None, private_token=None, api_version=4, section=None):
8
+ self.url = url
9
+ self.host = urlparse(url).hostname
10
+ self.use_ldap = use_ldap
11
+ self.username = username
12
+ self.password = password
13
+ self.private_token = private_token
14
+ self.api_version = api_version
15
+ self.section = section
16
+
17
+ self.session = requests.Session()
18
+ self.gitlab = None
19
+ self.id = None
20
+ self.is_admin = False
21
+ self.is_bot = False
22
+ self.is_authenticated = False
23
+
24
+ @property
25
+ def label(self):
26
+ return f"{self.username}@{self.host}"
27
+
28
+ def auth(self, private_token=None):
29
+ if private_token is not None:
30
+ self.private_token = private_token
31
+
32
+ self.gitlab = gitlab.Gitlab(self.url, private_token=self.private_token, keep_base_url=True, api_version=self.api_version)
33
+ self.gitlab.auth()
34
+ self.is_authenticated = True
35
+
36
+ user = self.gitlab.user
37
+ self.id = user.id
38
+ self.username = user.username
39
+ self.section = self.section if self.section else f"{self.id}@{self.host}"
40
+ self.is_admin = getattr(self.gitlab.user, 'is_admin', False)
41
+ self.is_bot = getattr(self.gitlab.user, 'bot', False)
42
+
43
+ def get_csrf_token(self):
44
+ r = self.session.get(f"{self.url}/")
45
+ soup = BeautifulSoup(r.text, "html.parser")
46
+ return soup.find("meta", {"name": "csrf-token"})["content"]
47
+
48
+ def login(self):
49
+ csrf_token = self.get_csrf_token()
50
+ if self.use_ldap:
51
+ payload = {
52
+ "authenticity_token": csrf_token,
53
+ "username": self.username,
54
+ "password": self.password,
55
+ "user[remember_me]": 1,
56
+ }
57
+ self.session.post(f"{self.url}/users/auth/ldapmain/callback", data=payload)
58
+ else:
59
+ payload = {
60
+ "authenticity_token": csrf_token,
61
+ "user[login]": self.username,
62
+ "user[password]": self.password,
63
+ "user[remember_me]": 1,
64
+ }
65
+ self.session.post(f"{self.url}/users/sign_in", data=payload)
66
+
67
+ def create_pat(self, token_name="private token", token_scopes=["api", "read_repository", "write_repository"]):
68
+ csrf_token = self.get_csrf_token()
69
+ payload = {
70
+ "personal_access_token[name]": token_name,
71
+ "personal_access_token[scopes][]": token_scopes,
72
+ }
73
+ headers = {"X-CSRF-Token": csrf_token}
74
+ r = self.session.post(
75
+ f"{self.url}/-/user_settings/personal_access_tokens",
76
+ data=payload,
77
+ headers=headers
78
+ )
79
+
80
+ try:
81
+ response_json = r.json()
82
+ if "new_token" in response_json:
83
+ return response_json.get("new_token")
84
+ else:
85
+ return response_json.get("token")
86
+ except ValueError:
87
+ return None
88
+
89
+ def add_ssh_key(self, title, key):
90
+ self.gitlab.user.keys.create({'title': title, 'key': key})
91
+
92
+ def to_dict(self):
93
+ return {
94
+ key: value
95
+ for key, value in {
96
+ "url": self.url,
97
+ "username": self.username,
98
+ "password": self.password,
99
+ "private_token": self.private_token,
100
+ "is_admin": self.is_admin,
101
+ "use_ldap": self.use_ldap,
102
+ "api_version": self.api_version,
103
+ }.items()
104
+ if value is not None
105
+ }
106
+
107
+ @classmethod
108
+ def from_dict(cls, data):
109
+ return cls(
110
+ url=data.get("url"),
111
+ username=data.get("username"),
112
+ password=data.get("password"),
113
+ private_token=data.get("private_token"),
114
+ use_ldap=(True if data.get("use_ldap") == "True" else False),
115
+ api_version=int(data.get("api_version", 4)),
116
+ section=data.get("_name", None)
117
+ )
118
+
119
+ def __repr__(self):
120
+ return self.to_dict().__repr__()
labrat/core/config.py ADDED
@@ -0,0 +1,63 @@
1
+ import configparser
2
+ from pathlib import Path
3
+
4
+ from labrat.core.agent import Agent
5
+ from labrat.core.utils import obj_filter
6
+
7
+ class Config:
8
+ def __init__(self, config_file="~/.python-gitlab.cfg", preauth=False, authed_only=False):
9
+ self.config_file = str(Path(config_file).expanduser())
10
+ self._config = configparser.ConfigParser()
11
+ self._config.read(self.config_file)
12
+ self.preauth = preauth
13
+ self.authed_only = authed_only
14
+
15
+ def __getitem__(self, section):
16
+ try:
17
+ return self._config[section]
18
+ except KeyError:
19
+ return {}
20
+
21
+ def __setitem__(self, section, value):
22
+ self._config[section] = value
23
+ self._save()
24
+
25
+ def _save(self):
26
+ with open(self.config_file, "w") as file:
27
+ self._config.write(file)
28
+
29
+ def __iter__(self, filter=None):
30
+ for section in self.sections():
31
+ if section not in ["DEFAULT", "global"]: # Skip special sections
32
+
33
+ data = self[section]
34
+ agent = Agent.from_dict(data) # Create an Agent instance from the section data
35
+
36
+ if self.preauth or self.authed_only:
37
+ try:
38
+ agent.auth()
39
+ except Exception as e:
40
+ pass
41
+
42
+ if filter and not obj_filter(agent, filter):
43
+ continue
44
+
45
+ if agent.is_authenticated or not self.authed_only:
46
+ yield section, agent
47
+
48
+ def filter(self, filter):
49
+ return self.__iter__(filter)
50
+
51
+ def sections(self):
52
+ return self._config.sections()
53
+
54
+ def has_section(self, section):
55
+ return self._config.has_section(section)
56
+
57
+ def remove_section(self, section):
58
+ if section in self._config:
59
+ self._config.remove_section(section)
60
+ self._save()
61
+ return True
62
+ else:
63
+ return False
labrat/core/utils.py ADDED
@@ -0,0 +1,106 @@
1
+ import re
2
+
3
+ def parse_host_range(host_pattern):
4
+ """Parse a host pattern and generate a list of individual host addresses.
5
+
6
+ Keyword arguments:
7
+ - host_pattern: The host pattern to parse (e.g., "team{01..15}.ccdc.local")
8
+ """
9
+
10
+ hosts = []
11
+
12
+ # Match brace expansion pattern (e.g., "team{01..15}.ccdc.local")
13
+ brace_expansion_match = re.match(r"(.*)\{(\d+)\.\.(\d+)\}(.*)", host_pattern)
14
+ if brace_expansion_match:
15
+ prefix = brace_expansion_match.group(1)
16
+ start = brace_expansion_match.group(2)
17
+ end = brace_expansion_match.group(3)
18
+ suffix = brace_expansion_match.group(4)
19
+
20
+ # Padding for leading zeros
21
+ padding = max(len(start), len(end)) if start[0] == '0' or end[0] == '0' else 0
22
+
23
+ for i in range(int(start), int(end) + 1):
24
+ hosts.append(f"{prefix}{i:0{padding}d}{suffix}")
25
+ return hosts
26
+
27
+ # If no range is detected, return the host as-is
28
+ return [host_pattern]
29
+
30
+ def obj_filter(obj, queries):
31
+ """Filter an object based on a list of filters. Filters can include simple substrings or field selection and regex patterns.
32
+
33
+ Filter criteria:
34
+ - Searching on all fields (e.g., `j\\.doe`)
35
+ - Field selection (e.g., `username=j.doe`)
36
+
37
+ Operators:
38
+ - `=`: Equal
39
+ - `!=`: Not equal
40
+ - `=~`: Regex match
41
+ - `!~`: Regex not match
42
+
43
+ Keyword arguments:
44
+ - obj: The object to filter.
45
+ - queries: A list of filter strings to apply.
46
+ """
47
+
48
+ result = False
49
+ for query in queries:
50
+ equals_op = True
51
+ regex_op = True
52
+
53
+ # Operator parsing
54
+ parsed_query = re.match(r"([a-zA-Z_]+)(!=|=~|!~|=)(.*)", query)
55
+ if parsed_query:
56
+ field = parsed_query.group(1)
57
+ operator = parsed_query.group(2)
58
+ pattern = parsed_query.group(3)
59
+
60
+ equals_op = not operator[0] == "!"
61
+ regex_op = operator[-1] == "~"
62
+
63
+ value = getattr(obj, field, None)
64
+ else:
65
+ pattern = query
66
+ value = get_attrs(obj)
67
+
68
+ # Ensure value is searchable
69
+ if value is not None:
70
+ # Exclude keys from search
71
+ if isinstance(value, dict):
72
+ value = str(value.values())
73
+ else:
74
+ value = str(value)
75
+
76
+ # Regex or literal search
77
+ if regex_op:
78
+ search = re.search(pattern, value, flags=re.IGNORECASE) is not None
79
+ else:
80
+ search = pattern.casefold() == value.casefold()
81
+
82
+ # Negate search if using not-operator
83
+ if search == equals_op:
84
+ result = True
85
+ else:
86
+ return False
87
+ elif equals_op:
88
+ return False
89
+
90
+ return result
91
+
92
+ def get_attrs(obj):
93
+ """Get attributes of an object as a dictionary.
94
+
95
+ Keyword arguments:
96
+ - obj: The object to get attributes from.
97
+ """
98
+ attrs = {}
99
+ if hasattr(obj, "_attrs"):
100
+ attrs.update(obj._attrs) # Include extended GitLab attributes
101
+ if hasattr(obj, "_updated_attrs"):
102
+ attrs.update(obj._updated_attrs) # Include updated attributes
103
+ if not attrs:
104
+ attrs = {k: v for k, v in vars(obj).items() if not hasattr(v, '__dict__')}
105
+
106
+ return attrs if attrs else None
labrat/main.py ADDED
@@ -0,0 +1,19 @@
1
+ # labrat/main.py
2
+ import argparse
3
+ from labrat.cli import agents, auth, projects, users
4
+
5
+ def main():
6
+ parser = argparse.ArgumentParser(
7
+ prog="labrat",
8
+ description="LabRat: GitLab exploitation orchestrator"
9
+ )
10
+ subparsers = parser.add_subparsers(dest="command", required=True)
11
+
12
+ for command in [agents, auth, projects, users]:
13
+ command.build_parser(subparsers)
14
+
15
+ args = parser.parse_args()
16
+ args.func(args)
17
+
18
+ if __name__ == "__main__":
19
+ main()