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.
- gitlabrat-1.0.dist-info/METADATA +72 -0
- gitlabrat-1.0.dist-info/RECORD +22 -0
- gitlabrat-1.0.dist-info/WHEEL +4 -0
- gitlabrat-1.0.dist-info/entry_points.txt +2 -0
- gitlabrat-1.0.dist-info/licenses/LICENSE +674 -0
- labrat/__init__.py +0 -0
- labrat/cli/__init__.py +0 -0
- labrat/cli/agents.py +60 -0
- labrat/cli/auth.py +60 -0
- labrat/cli/common.py +76 -0
- labrat/cli/projects.py +95 -0
- labrat/cli/users.py +40 -0
- labrat/controllers/__init__.py +0 -0
- labrat/controllers/agents.py +47 -0
- labrat/controllers/auth.py +55 -0
- labrat/controllers/projects.py +164 -0
- labrat/controllers/users.py +68 -0
- labrat/core/__init__.py +0 -0
- labrat/core/agent.py +120 -0
- labrat/core/config.py +63 -0
- labrat/core/utils.py +106 -0
- labrat/main.py +19 -0
labrat/cli/agents.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from labrat.cli import common
|
|
2
|
+
from labrat.controllers.agents import Agents
|
|
3
|
+
|
|
4
|
+
def build_parser(parsers):
|
|
5
|
+
parser = parsers.add_parser("agents", help="Manage GitLab agents")
|
|
6
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
7
|
+
|
|
8
|
+
list_parser = common.add_filtered_parser(subparsers, "list", handle_list_args, aliases=["ls"], help="List GitLab servers", filter_required=False)
|
|
9
|
+
|
|
10
|
+
delete_parser = common.add_filtered_parser(subparsers, "delete", handle_delete_args, aliases=["rm"], help="Delete GitLab server from config")
|
|
11
|
+
|
|
12
|
+
add_key_parser = common.add_filtered_parser(subparsers, "add-key", handle_add_key_args, help="Add SSH key to the user account", filter_required=False)
|
|
13
|
+
key_group = add_key_parser.add_mutually_exclusive_group(required=True)
|
|
14
|
+
key_group.add_argument("-k", "--key", required=False, help="Public SSH key to add")
|
|
15
|
+
key_group.add_argument("-K", "--key-file", required=False, help="Path to public SSH key file")
|
|
16
|
+
|
|
17
|
+
add_key_parser.add_argument("-t", "--title", required=False, help="Title for the SSH key", default="SSH Key")
|
|
18
|
+
|
|
19
|
+
parser.set_defaults(controller=Agents())
|
|
20
|
+
return parser
|
|
21
|
+
|
|
22
|
+
def handle_list_args(args):
|
|
23
|
+
# Prepare table
|
|
24
|
+
headers = ["Host", "ID", "Username", "Name", "Authenticated", "Is Admin", "Is Bot", "Password", "Private Token"]
|
|
25
|
+
data = []
|
|
26
|
+
|
|
27
|
+
for agent in args.controller.list(args.filter):
|
|
28
|
+
data.append([
|
|
29
|
+
agent.host,
|
|
30
|
+
agent.id,
|
|
31
|
+
agent.username,
|
|
32
|
+
agent.gitlab.user.name if agent.is_authenticated else "-",
|
|
33
|
+
agent.is_authenticated,
|
|
34
|
+
agent.is_admin,
|
|
35
|
+
agent.is_bot,
|
|
36
|
+
agent.password,
|
|
37
|
+
agent.private_token
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
common.print_table(headers, data, "Host")
|
|
41
|
+
|
|
42
|
+
def handle_delete_args(args):
|
|
43
|
+
# Delete configs for filtered sections
|
|
44
|
+
for agent in args.controller.delete(args.filter):
|
|
45
|
+
print(f"[-] Deleted {agent.label} from config")
|
|
46
|
+
|
|
47
|
+
def handle_add_key_args(args):
|
|
48
|
+
# Prepare SSH key
|
|
49
|
+
if args.key:
|
|
50
|
+
key = args.key
|
|
51
|
+
elif args.key_file:
|
|
52
|
+
with open(args.key_file, "r") as f:
|
|
53
|
+
key = f.read().strip()
|
|
54
|
+
|
|
55
|
+
# Add SSH key to agents
|
|
56
|
+
for agent, err in args.controller.add_ssh_key(args.filter, args.title, key):
|
|
57
|
+
if err:
|
|
58
|
+
print(f"[-] Failed to add SSH key to {agent.label}: {err}")
|
|
59
|
+
else:
|
|
60
|
+
print(f"[+] Added SSH key to {agent.label}")
|
labrat/cli/auth.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from labrat.controllers.auth import Auth
|
|
2
|
+
from labrat.core.utils import parse_host_range
|
|
3
|
+
|
|
4
|
+
def build_parser(parsers):
|
|
5
|
+
parser = parsers.add_parser("auth", help="Authenticate to GitLab server(s)")
|
|
6
|
+
|
|
7
|
+
parser.add_argument("-t", "--target", required=False, nargs="+", action="extend", help="GitLab server URL or pattern")
|
|
8
|
+
parser.add_argument("-T", "--target-file", required=False, help="Path to file containing GitLab server URLs or patterns")
|
|
9
|
+
parser.add_argument("-u", "--username", required=False, help="Username or e-mail for authentication")
|
|
10
|
+
parser.add_argument("-p", "--password", required=False, help="Password for authentication")
|
|
11
|
+
parser.add_argument("-C", "--combo-list", required=False, help="Path to the combo list file")
|
|
12
|
+
parser.add_argument("-l", "--use-ldap", action="store_true", help="Use LDAP for authentication")
|
|
13
|
+
parser.add_argument("-r", "--re-auth", action="store_true", help="Re-authenticate with stored credentials")
|
|
14
|
+
parser.add_argument("-n", "--token-name", required=False, help="Name for the access token", default="private token")
|
|
15
|
+
parser.add_argument("-s", "--scopes", required=False, help="Comma-separated list of scopes for the access token", default="api,read_repository,write_repository")
|
|
16
|
+
|
|
17
|
+
parser.set_defaults(func=handle_args, _parser=parser)
|
|
18
|
+
parser.set_defaults(controller=Auth())
|
|
19
|
+
return parser
|
|
20
|
+
|
|
21
|
+
def handle_args(args):
|
|
22
|
+
has_targets = args.target or args.target_file
|
|
23
|
+
has_credentials = (args.username and args.password) or args.combo_list
|
|
24
|
+
can_auth = has_targets and has_credentials
|
|
25
|
+
|
|
26
|
+
if args.re_auth or can_auth:
|
|
27
|
+
auth(args)
|
|
28
|
+
else:
|
|
29
|
+
getattr(args, "_parser", None).print_help()
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
def auth(args):
|
|
33
|
+
# Build the list of users
|
|
34
|
+
users = []
|
|
35
|
+
if args.username:
|
|
36
|
+
users = [(args.username, args.password)]
|
|
37
|
+
elif args.combo_list:
|
|
38
|
+
with open(args.combo_list, "r") as f:
|
|
39
|
+
for line in f:
|
|
40
|
+
username, password = line.strip().split(":")
|
|
41
|
+
users.append((username, password))
|
|
42
|
+
|
|
43
|
+
# Build the list of targets
|
|
44
|
+
targets = []
|
|
45
|
+
if args.target:
|
|
46
|
+
for target in args.target:
|
|
47
|
+
targets.extend(parse_host_range(target))
|
|
48
|
+
elif args.target_file:
|
|
49
|
+
with open(args.target_file, "r") as f:
|
|
50
|
+
for line in f:
|
|
51
|
+
targets.extend(parse_host_range(line.strip()))
|
|
52
|
+
|
|
53
|
+
scopes = args.scopes.split(",") if args.scopes else []
|
|
54
|
+
|
|
55
|
+
# Iterate over each user and target
|
|
56
|
+
for agent, err in args.controller.reauth(token_name=args.token_name, token_scopes=scopes, targets=targets, users=[user[0] for user in users]) if args.re_auth else args.controller.auth(targets, users, token_name=args.token_name, token_scopes=scopes, use_ldap=args.use_ldap):
|
|
57
|
+
if err:
|
|
58
|
+
print(f"[-] Authentication failed for {agent.label}: {err}")
|
|
59
|
+
else:
|
|
60
|
+
print(f"[+] Authenticated {agent.label}{' (admin)' if agent.is_admin else ''} with {agent.private_token}")
|
labrat/cli/common.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
def add_filtered_parser(subparsers, name, handler, aliases=[], help=None, filter_required=True, all_help="All items", filter_help="Filter by substring"):
|
|
4
|
+
parser = subparsers.add_parser(name, aliases=aliases, help=help)
|
|
5
|
+
|
|
6
|
+
filter_group = parser.add_mutually_exclusive_group(required=filter_required)
|
|
7
|
+
|
|
8
|
+
if filter_required:
|
|
9
|
+
filter_group.add_argument("-a", "--all", action="store_true", help=all_help)
|
|
10
|
+
|
|
11
|
+
filter_group.add_argument("-f", "--filter", action="append", help=filter_help)
|
|
12
|
+
|
|
13
|
+
parser.set_defaults(func=handler, _parser=parser)
|
|
14
|
+
return parser
|
|
15
|
+
|
|
16
|
+
def print_table(headers, data, sort_key=None):
|
|
17
|
+
"""
|
|
18
|
+
Print a table with the given data and headers.
|
|
19
|
+
"""
|
|
20
|
+
column_widths = [max(len(re.sub(r'\x1b\[[0-9;]*m', '', str(x))) for x in col) for col in zip(*([headers] + data))]
|
|
21
|
+
|
|
22
|
+
# Print header
|
|
23
|
+
if headers:
|
|
24
|
+
print(" " + " ".join(h.ljust(w) for h, w in zip(headers, column_widths)) + " ")
|
|
25
|
+
print(" ".join("=" * (w + 1) for w in column_widths))
|
|
26
|
+
|
|
27
|
+
# Sort data by the specified key
|
|
28
|
+
if sort_key is not None:
|
|
29
|
+
data = sorted(data, key=lambda x: x[headers.index(sort_key)])
|
|
30
|
+
|
|
31
|
+
# Print data rows
|
|
32
|
+
for row in data:
|
|
33
|
+
print(" " + " ".join(str(x).ljust(w) for x, w in zip(row, column_widths)) + " ")
|
|
34
|
+
|
|
35
|
+
class Colors:
|
|
36
|
+
RESET = "\x1b[0m"
|
|
37
|
+
BRIGHT_RED = "\x1b[0;91m"
|
|
38
|
+
RED = "\x1b[0;31m"
|
|
39
|
+
GREEN = "\x1b[0;32m"
|
|
40
|
+
YELLOW = "\x1b[0;33m"
|
|
41
|
+
BLUE = "\x1b[0;34m"
|
|
42
|
+
MAGENTA = "\x1b[0;35m"
|
|
43
|
+
CYAN = "\x1b[0;36m"
|
|
44
|
+
WHITE = "\x1b[0;37m"
|
|
45
|
+
|
|
46
|
+
def color_access_level(level, text):
|
|
47
|
+
shades = [
|
|
48
|
+
Colors.RESET, # guest - none
|
|
49
|
+
Colors.RESET, # planner - none
|
|
50
|
+
Colors.RESET, # reporter - none
|
|
51
|
+
Colors.GREEN, # developer - green
|
|
52
|
+
Colors.YELLOW, # maintainer - yellow
|
|
53
|
+
Colors.BRIGHT_RED, # owner - bright red (highest)
|
|
54
|
+
]
|
|
55
|
+
idx = min(len(shades)-1, max(0, (level // 10)))
|
|
56
|
+
return f"{shades[idx]}{text}{Colors.RESET}"
|
|
57
|
+
|
|
58
|
+
def color_diff(diff):
|
|
59
|
+
lines = []
|
|
60
|
+
for line in diff.splitlines():
|
|
61
|
+
if line.startswith("@@"):
|
|
62
|
+
match = re.match(r"(@@[^@]*@@)(.*)", line)
|
|
63
|
+
if match:
|
|
64
|
+
header = match.group(1)
|
|
65
|
+
tail = match.group(2)
|
|
66
|
+
lines.append(f"{Colors.CYAN}{header}{Colors.RESET}{tail}")
|
|
67
|
+
else:
|
|
68
|
+
lines.append(line)
|
|
69
|
+
elif line.startswith("+"):
|
|
70
|
+
lines.append(f"{Colors.GREEN}{line}{Colors.RESET}") # Green for additions
|
|
71
|
+
elif line.startswith("-"):
|
|
72
|
+
lines.append(f"{Colors.RED}{line}{Colors.RESET}") # Red for deletions
|
|
73
|
+
else:
|
|
74
|
+
lines.append(line)
|
|
75
|
+
|
|
76
|
+
return "\n".join(lines)
|
labrat/cli/projects.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import gitlab
|
|
3
|
+
|
|
4
|
+
from labrat.cli import common
|
|
5
|
+
from labrat.controllers.projects import Projects
|
|
6
|
+
|
|
7
|
+
def build_parser(parsers):
|
|
8
|
+
parser = parsers.add_parser("projects", help="Manage GitLab projects")
|
|
9
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
10
|
+
|
|
11
|
+
list_parser = common.add_filtered_parser(subparsers, "list", handle_list_args, aliases=["ls"], help="List GitLab projects", filter_required=False)
|
|
12
|
+
list_parser.add_argument("-m", "--min-access-level", required=False, type=int, help="Minimum access level to filter projects", default=0)
|
|
13
|
+
list_parser.add_argument("-u", "--user", action="append", required=False, help="Filter agent performing action")
|
|
14
|
+
|
|
15
|
+
clone_parser = common.add_filtered_parser(subparsers, "clone", handle_clone_args, help="Clone GitLab repositories")
|
|
16
|
+
clone_parser.add_argument("-o", "--output", required=False, help="Output location for cloned repositories", default='./')
|
|
17
|
+
clone_parser.add_argument("-u", "--user", action="append", required=False, help="Filter agent performing action")
|
|
18
|
+
|
|
19
|
+
create_token_parser = common.add_filtered_parser(subparsers, "create_token", handle_create_token_args, help="Create an access token", filter_required=True)
|
|
20
|
+
create_token_parser.add_argument("-l", "--access-level", required=False, type=int, help="Access level for the access token", default=50)
|
|
21
|
+
create_token_parser.add_argument("-d", "--days", required=False, type=int, help="Number of days until the token expires", default=60)
|
|
22
|
+
create_token_parser.add_argument("-n", "--token-name", required=False, help="Name for the access token", default="project_bot")
|
|
23
|
+
create_token_parser.add_argument("-s", "--scopes", required=False, help="Comma-separated list of scopes for the access token", default="api,read_repository,write_repository")
|
|
24
|
+
create_token_parser.add_argument("-u", "--user", action="append", required=False, help="Filter agent performing action")
|
|
25
|
+
|
|
26
|
+
update_parser = common.add_filtered_parser(subparsers, "update", handle_update_args, help="Update GitLab repositories procedurally", filter_required=True)
|
|
27
|
+
update_parser.add_argument("-F", "--file", required=True, help="Path to the remote file to update")
|
|
28
|
+
|
|
29
|
+
mechanism_group = update_parser.add_mutually_exclusive_group(required=True)
|
|
30
|
+
mechanism_group.add_argument("-c", "--content", required=False, help="Text content to replace the file with")
|
|
31
|
+
mechanism_group.add_argument("-C", "--content-file", required=False, help="Path to replacement file")
|
|
32
|
+
mechanism_group.add_argument("-p", "--pattern", required=False, help="String or regex pattern to find in the file")
|
|
33
|
+
|
|
34
|
+
update_parser.add_argument("-r", "--replace", required=False, help="String or pattern to replace the found text")
|
|
35
|
+
|
|
36
|
+
update_parser.add_argument("-m", "--commit-message", required=False, help="Commit message for the update", default="Update")
|
|
37
|
+
update_parser.add_argument("-b", "--branch", required=False, help="Branch to update", default="main")
|
|
38
|
+
update_parser.add_argument("-u", "--user", action="append", required=False, help="Filter agent performing action")
|
|
39
|
+
|
|
40
|
+
parser.set_defaults(controller=Projects())
|
|
41
|
+
return subparsers
|
|
42
|
+
|
|
43
|
+
def handle_list_args(args):
|
|
44
|
+
headers = ["Host", "ID", "Path with Namespace", "Access Level", "Agents"]
|
|
45
|
+
data = []
|
|
46
|
+
|
|
47
|
+
for project in args.controller.list(args.filter, args.user):
|
|
48
|
+
# color each username by that agent's access level (higher -> brighter)
|
|
49
|
+
usernames = []
|
|
50
|
+
for agent in project.agents:
|
|
51
|
+
colored_username = common.color_access_level(agent.access_level, f"{agent.username} ({agent.access_level})")
|
|
52
|
+
usernames.append(colored_username)
|
|
53
|
+
|
|
54
|
+
# Add project information to data table
|
|
55
|
+
data.append([
|
|
56
|
+
project.host,
|
|
57
|
+
project.id,
|
|
58
|
+
project.path_with_namespace,
|
|
59
|
+
f"{project.access_level} ({gitlab.const.AccessLevel(project.access_level).name.lower()})",
|
|
60
|
+
", ".join(usernames)
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
common.print_table(headers, data)
|
|
64
|
+
|
|
65
|
+
def handle_clone_args(args):
|
|
66
|
+
for project, err in args.controller.clone(args.output, args.user, args.filter):
|
|
67
|
+
if err:
|
|
68
|
+
print(f"[!] Failed to clone {project.web_url}: {err}")
|
|
69
|
+
else:
|
|
70
|
+
print(f"[+] Successfully cloned {project.web_url} to {project.to_path}")
|
|
71
|
+
|
|
72
|
+
def handle_create_token_args(args):
|
|
73
|
+
expiration = (datetime.datetime.now() + datetime.timedelta(days=args.days)).strftime("%Y-%m-%d")
|
|
74
|
+
for project, agent, err in args.controller.create_token(args.token_name, args.access_level, args.scopes.split(","), expiration, args.user, args.filter):
|
|
75
|
+
if err:
|
|
76
|
+
print(f"[!] Failed to create access token for {project.web_url}: {err}")
|
|
77
|
+
else:
|
|
78
|
+
print(f"[+] Successfully created access token for {project.web_url}: {agent.private_token}")
|
|
79
|
+
|
|
80
|
+
def handle_update_args(args):
|
|
81
|
+
content = None
|
|
82
|
+
if args.content:
|
|
83
|
+
content = args.content
|
|
84
|
+
elif args.content_file:
|
|
85
|
+
with open(args.content_file, "r") as f:
|
|
86
|
+
content = f.read()
|
|
87
|
+
|
|
88
|
+
for project, commit, err in args.controller.update(file_path=args.file, content=content, pattern=args.pattern, replace=args.replace, branch=args.branch, commit_message=args.commit_message, agent_filter=args.user, filter=args.filter):
|
|
89
|
+
if err:
|
|
90
|
+
print(f"[!] Failed to update {project.web_url}: {err}")
|
|
91
|
+
else:
|
|
92
|
+
print(f"[+] Successfully updated {project.web_url}:")
|
|
93
|
+
diff = commit.diff()
|
|
94
|
+
if diff:
|
|
95
|
+
print(common.color_diff(diff[0].get("diff")))
|
labrat/cli/users.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from labrat.cli import common
|
|
2
|
+
from labrat.controllers.users import Users
|
|
3
|
+
|
|
4
|
+
def build_parser(parsers):
|
|
5
|
+
parser = parsers.add_parser("users", help="Manage GitLab users")
|
|
6
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
7
|
+
|
|
8
|
+
list_parser = common.add_filtered_parser(subparsers, "list", handle_list_args, aliases=["ls"], help="List GitLab users", filter_required=False)
|
|
9
|
+
|
|
10
|
+
create_token_parser = common.add_filtered_parser(subparsers, "create_token", handle_create_token_args, help="Create an access token", filter_required=True)
|
|
11
|
+
create_token_parser.add_argument("-n", "--token-name", required=False, help="Name for the access token", default="private token")
|
|
12
|
+
create_token_parser.add_argument("-s", "--scopes", required=False, help="Comma-separated list of scopes for the access token", default="api,read_repository,write_repository")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
parser.set_defaults(controller=Users())
|
|
16
|
+
return parser
|
|
17
|
+
|
|
18
|
+
def handle_list_args(args):
|
|
19
|
+
headers = ["Host", "ID", "Username", "Name", "Is Agent", "Is Admin", "Is Bot"]
|
|
20
|
+
data = []
|
|
21
|
+
|
|
22
|
+
for user in args.controller.list(filter=args.filter):
|
|
23
|
+
data.append([
|
|
24
|
+
user.host,
|
|
25
|
+
user.id,
|
|
26
|
+
user.username,
|
|
27
|
+
user.name,
|
|
28
|
+
getattr(user, "is_agent", "-"),
|
|
29
|
+
getattr(user, "is_admin", "-"),
|
|
30
|
+
getattr(user, "bot", "-")
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
common.print_table(headers, data, "Host")
|
|
34
|
+
|
|
35
|
+
def handle_create_token_args(args):
|
|
36
|
+
for agent, err in args.controller.create_token(args.token_name, args.scopes.split(","), filter=args.filter):
|
|
37
|
+
if err:
|
|
38
|
+
print(f"[-] Failed to create access token: {err}")
|
|
39
|
+
else:
|
|
40
|
+
print(f"[+] Created access token for {agent.label}: {agent.private_token}")
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from labrat.core.agent import Agent
|
|
2
|
+
from labrat.core.config import Config
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
class Agents:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.config = Config(preauth=True)
|
|
8
|
+
|
|
9
|
+
def list(self, filter=None):
|
|
10
|
+
"""List Labrat agents in config.
|
|
11
|
+
|
|
12
|
+
Keyword arguments:
|
|
13
|
+
- filter: Regex filter and field selection passed to `utils.obj_filter()`
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
for section, agent in self.config.filter(filter):
|
|
17
|
+
yield agent
|
|
18
|
+
|
|
19
|
+
def delete(self, filter):
|
|
20
|
+
"""Delete Labrat agents from config.
|
|
21
|
+
|
|
22
|
+
Keyword arguments:
|
|
23
|
+
- filter: Regex filter and field selection passed to `utils.obj_filter()`
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
for section, agent in self.config.filter(filter):
|
|
27
|
+
self.config.remove_section(section)
|
|
28
|
+
yield agent
|
|
29
|
+
|
|
30
|
+
def add_ssh_key(self, filter, title, key):
|
|
31
|
+
"""Add an SSH key to each agent matching the filter.
|
|
32
|
+
|
|
33
|
+
Keyword arguments:
|
|
34
|
+
- filter: Regex filter and field selection passed to `utils.obj_filter()`
|
|
35
|
+
- title: Title of the SSH key.
|
|
36
|
+
- key: The SSH key to add.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
for section, agent in self.config.filter(filter):
|
|
40
|
+
if not agent.is_authenticated:
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
agent.add_ssh_key(title, key)
|
|
45
|
+
yield agent, None
|
|
46
|
+
except Exception as e:
|
|
47
|
+
yield agent, e
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from labrat.core.agent import Agent
|
|
2
|
+
from labrat.core.config import Config
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
class Auth:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.config = Config()
|
|
8
|
+
|
|
9
|
+
def auth(self, targets, users, token_name, token_scopes, use_ldap=False):
|
|
10
|
+
"""Authenticate and create a PAT for each user on each target.
|
|
11
|
+
|
|
12
|
+
Keyword arguments:
|
|
13
|
+
- targets: List of target URLs to authenticate against.
|
|
14
|
+
- users: List of (username, password) tuples for authentication.
|
|
15
|
+
- token_name: Name for the created Access Token.
|
|
16
|
+
- token_scopes: Scopes for the created Access Token.
|
|
17
|
+
- use_ldap: Whether to use LDAP for authentication (default: False).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
for username, password in users:
|
|
21
|
+
for target in targets:
|
|
22
|
+
agent = Agent(target, use_ldap, username, password)
|
|
23
|
+
try:
|
|
24
|
+
agent.login()
|
|
25
|
+
agent.auth(private_token=agent.create_pat(token_name, token_scopes))
|
|
26
|
+
self.config[agent.section] = agent.to_dict()
|
|
27
|
+
yield agent, None
|
|
28
|
+
except Exception as e:
|
|
29
|
+
yield agent, e
|
|
30
|
+
|
|
31
|
+
def reauth(self, token_name, token_scopes, targets=None, users=None):
|
|
32
|
+
"""Re-authenticate existing agents.
|
|
33
|
+
|
|
34
|
+
Keyword arguments:
|
|
35
|
+
- targets: List of target URLs to re-authenticate against.
|
|
36
|
+
- users: List of usernames to re-authenticate.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
for section, agent in self.config:
|
|
40
|
+
# Filter by target if specified
|
|
41
|
+
if targets and agent.url not in targets:
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
# Filter by username if specified
|
|
45
|
+
if users and agent.username not in users:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
if not agent.is_authenticated:
|
|
50
|
+
agent.login()
|
|
51
|
+
agent.auth(private_token=agent.create_pat(token_name, token_scopes))
|
|
52
|
+
self.config[section] = agent.to_dict()
|
|
53
|
+
yield agent, None
|
|
54
|
+
except Exception as e:
|
|
55
|
+
yield agent, e
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import os
|
|
3
|
+
import git
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from labrat.core.agent import Agent
|
|
7
|
+
from labrat.core.config import Config
|
|
8
|
+
from labrat.core.utils import obj_filter
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
class Projects:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.config = Config(authed_only=True)
|
|
14
|
+
|
|
15
|
+
def list(self, filter=None, agent_filter=None, min_level=0):
|
|
16
|
+
"""List projects accessible by agents.
|
|
17
|
+
|
|
18
|
+
Keyword arguments:
|
|
19
|
+
- filter: Regex filter and field selection passed to `utils.obj_filter()` for project objects
|
|
20
|
+
- agent_filter: Regex filter and field selection passed to `utils.obj_filter()` for agent objects
|
|
21
|
+
- min_level: Minimum desired access level for agent to return a project
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
repo_map = {}
|
|
25
|
+
for section, agent in self.config.filter(agent_filter):
|
|
26
|
+
projects = agent.gitlab.projects.list(all=True)
|
|
27
|
+
for project in projects:
|
|
28
|
+
access_level = self.get_project_access_level(agent, project)
|
|
29
|
+
if access_level < min_level:
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
# Agent enrichment with shallow copy
|
|
33
|
+
agent_copy = copy.copy(agent)
|
|
34
|
+
agent_copy.access_level = access_level
|
|
35
|
+
|
|
36
|
+
# Project enrichment
|
|
37
|
+
project.host = agent.host
|
|
38
|
+
|
|
39
|
+
# Add project and agent to map
|
|
40
|
+
if project.web_url not in repo_map:
|
|
41
|
+
project.agents = [agent_copy]
|
|
42
|
+
project.access_level = access_level
|
|
43
|
+
repo_map[project.web_url] = project
|
|
44
|
+
else:
|
|
45
|
+
repo_map[project.web_url].agents.append(agent_copy)
|
|
46
|
+
repo_map[project.web_url].access_level = max(repo_map[project.web_url].access_level, access_level)
|
|
47
|
+
|
|
48
|
+
return [proj for proj in repo_map.values() if not filter or obj_filter(proj, filter)]
|
|
49
|
+
|
|
50
|
+
def clone(self, output, agent_filter=None, filter=None):
|
|
51
|
+
"""Clone projects accessible by agents.
|
|
52
|
+
|
|
53
|
+
Keyword arguments:
|
|
54
|
+
- output: Output directory for cloned projects. A nested structure of `<hostname>/<namespace>/<project>` will be created inside.
|
|
55
|
+
- agent_filter: Regex filter and field selection passed to `utils.obj_filter()` for agent objects.
|
|
56
|
+
- filter: Regex filter and field selection passed to `utils.obj_filter()` for project objects.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
projects = self.list(filter=filter, agent_filter=agent_filter, min_level=15)
|
|
60
|
+
for project in projects:
|
|
61
|
+
agent = project.agents[0]
|
|
62
|
+
|
|
63
|
+
url = urlparse(agent.url)
|
|
64
|
+
clone_url = f"{url.scheme}://{agent.username}:{agent.private_token}@{url.netloc}/{project.path_with_namespace}.git"
|
|
65
|
+
to_path = f"{output}{'/' if output[-1] != '/' else ''}{url.hostname}/{project.path_with_namespace}"
|
|
66
|
+
project.clone_url = clone_url
|
|
67
|
+
project.to_path = to_path
|
|
68
|
+
|
|
69
|
+
if os.path.isdir(to_path):
|
|
70
|
+
yield project, f"destination path '{to_path}' already exists"
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
git.Repo.clone_from(clone_url, to_path)
|
|
75
|
+
yield project, None
|
|
76
|
+
except Exception as e:
|
|
77
|
+
yield project, e
|
|
78
|
+
|
|
79
|
+
def create_token(self, name, access_level, scopes, expires_at, agent_filter=None, filter=None):
|
|
80
|
+
""" Create Project Access Tokens.
|
|
81
|
+
|
|
82
|
+
Keyword arguments:
|
|
83
|
+
- name: Name of the access token.
|
|
84
|
+
- access_level: Access level of the created PAT.
|
|
85
|
+
- scopes: List of scopes for the created PAT. (e.g. `['read_repository', 'write_repository']`)
|
|
86
|
+
- expires_at: ISO format expiration date for the created PAT.
|
|
87
|
+
- agent_filter: Regex filter and field selection passed to `utils.obj_filter()` for agent objects.
|
|
88
|
+
- filter: Regex filter and field selection passed to `utils.obj_filter()` for project objects.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
for project in self.list(filter=filter, agent_filter=agent_filter, min_level=40):
|
|
92
|
+
agent = project.agents[0]
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
req = project.access_tokens.create({
|
|
96
|
+
'name': name,
|
|
97
|
+
'access_level': access_level,
|
|
98
|
+
'scopes': scopes,
|
|
99
|
+
'expires_at': expires_at
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
section = f"{req.user_id}_{project.id}@{agent.host}"
|
|
103
|
+
project_agent = Agent(agent.url, username=name, private_token=req.token, section=section)
|
|
104
|
+
self.config[section] = project_agent.to_dict()
|
|
105
|
+
|
|
106
|
+
yield project, project_agent, None
|
|
107
|
+
except Exception as e:
|
|
108
|
+
yield project, None, e
|
|
109
|
+
|
|
110
|
+
def update(self, file_path, content=None, pattern=None, replace=None, branch=None, commit_message=None, agent_filter=None, filter=None):
|
|
111
|
+
"""Update file contents.
|
|
112
|
+
|
|
113
|
+
Keyword arguments:
|
|
114
|
+
- file_path: Path to the file in project to update.
|
|
115
|
+
- content: New content for the file.
|
|
116
|
+
- pattern: Regex pattern to match for replacement.
|
|
117
|
+
- replace: Replacement string for the regex pattern.
|
|
118
|
+
- branch: Branch to commit the changes to.
|
|
119
|
+
- commit_message: Commit message for the update.
|
|
120
|
+
- agent_filter: Regex filter and field selection passed to `utils.obj_filter()` for agent objects.
|
|
121
|
+
- filter: Regex filter and field selection passed to `utils.obj_filter()` for project objects.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
for project in self.list(filter=filter, agent_filter=agent_filter, min_level=30):
|
|
125
|
+
agent = project.agents[0]
|
|
126
|
+
try:
|
|
127
|
+
file = project.files.get(file_path=file_path, ref="main")
|
|
128
|
+
file_content = file.decode().decode("utf-8")
|
|
129
|
+
|
|
130
|
+
if content is not None:
|
|
131
|
+
new_content = content
|
|
132
|
+
elif pattern and replace is not None:
|
|
133
|
+
new_content = re.sub(pattern, replace, file_content, flags=re.MULTILINE)
|
|
134
|
+
else:
|
|
135
|
+
yield project, None, "No update method specified"
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
if new_content == file_content:
|
|
139
|
+
yield project, None, "Content is the same, skipping update"
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
commit = project.commits.create({
|
|
143
|
+
"branch": branch,
|
|
144
|
+
"commit_message": commit_message,
|
|
145
|
+
"actions": [
|
|
146
|
+
{
|
|
147
|
+
"action": "update",
|
|
148
|
+
"file_path": file_path,
|
|
149
|
+
"content": new_content
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
})
|
|
153
|
+
yield project, commit, None
|
|
154
|
+
except Exception as e:
|
|
155
|
+
yield project, None, e
|
|
156
|
+
|
|
157
|
+
def get_project_access_level(self, agent, project):
|
|
158
|
+
if project.permissions['project_access']:
|
|
159
|
+
access_level = int(project.permissions['project_access']['access_level'])
|
|
160
|
+
elif project.permissions['group_access']:
|
|
161
|
+
access_level = int(project.permissions['group_access']['access_level'])
|
|
162
|
+
else:
|
|
163
|
+
access_level = 60 if agent.is_admin else 15
|
|
164
|
+
return access_level
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from labrat.core.agent import Agent
|
|
2
|
+
from labrat.core.config import Config
|
|
3
|
+
from labrat.core.utils import obj_filter
|
|
4
|
+
from urllib.parse import urlparse
|
|
5
|
+
|
|
6
|
+
class Users:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.config = Config(authed_only=True)
|
|
9
|
+
|
|
10
|
+
def list(self, filter=None):
|
|
11
|
+
"""List users and additional information.
|
|
12
|
+
|
|
13
|
+
Keyword arguments:
|
|
14
|
+
- filter: Regex filter and field selection passed to `utils.obj_filter()`
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
data = dict()
|
|
18
|
+
for section, agent in self.config:
|
|
19
|
+
users = agent.gitlab.users.list(all=True)
|
|
20
|
+
for user in users:
|
|
21
|
+
self._user_enrichment(agent, user)
|
|
22
|
+
|
|
23
|
+
if user.section not in data.keys() or agent.is_admin:
|
|
24
|
+
data[user.section] = user
|
|
25
|
+
|
|
26
|
+
data = [user for user in data.values() if not filter or obj_filter(user, filter)]
|
|
27
|
+
|
|
28
|
+
return data
|
|
29
|
+
|
|
30
|
+
def create_token(self, name, scopes, filter=None):
|
|
31
|
+
"""Create Personal Access Tokens for other users.
|
|
32
|
+
|
|
33
|
+
Keyword arguments:
|
|
34
|
+
- name: Name of the access token.
|
|
35
|
+
- scopes: List of scopes for the access token.
|
|
36
|
+
- filter: Regex filter and field selection passed to `utils.obj_filter()`
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
for section, agent in self.config:
|
|
40
|
+
if not agent.is_admin:
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
for user in agent.gitlab.users.list(all=True):
|
|
44
|
+
self._user_enrichment(agent, user)
|
|
45
|
+
if user.is_agent:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
if filter and not obj_filter(user, filter):
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
req = user.personal_access_tokens.create({
|
|
53
|
+
"name": name,
|
|
54
|
+
"scopes": scopes
|
|
55
|
+
})
|
|
56
|
+
agent_user = Agent(agent.url, username=user.username, private_token=req.token)
|
|
57
|
+
self.config[user.section] = agent_user.to_dict()
|
|
58
|
+
yield agent_user, None
|
|
59
|
+
except Exception as e:
|
|
60
|
+
yield None, e
|
|
61
|
+
|
|
62
|
+
def _user_enrichment(self, agent, user):
|
|
63
|
+
user.url = agent.url
|
|
64
|
+
user.host = agent.host
|
|
65
|
+
user.section = f"{user.id}@{agent.host}"
|
|
66
|
+
user.label = f"{user.username}@{agent.host}"
|
|
67
|
+
user.is_agent = self.config.has_section(user.section)
|
|
68
|
+
user.is_bot = getattr(user, "bot", None)
|
labrat/core/__init__.py
ADDED
|
File without changes
|