audit-labs/audit-tools

A collection of scripts, queries, and other goodies you can use in an audit.

clone: git clone https://gitbay.org/audit-labs/audit-tools.git

8b78620c2c39c996007212616a586a8644ae7e33

signed_unknown_key

author: Christian Cleberg <hello@cleberg.net> · 2025-04-08T03:52:59Z
committer: <noreply@github.com>

Gitlab enhancements (#2)

* add various in-progress scripts for gitlab

* Commit from GitHub Actions (Ruff)

* add gitlab results for free-tier tools

* Commit from GitHub Actions (Ruff)

* add gitlab results for ultimate-tier tools

* Commit from GitHub Actions (Ruff)

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
 applications/gitlab/README.org            | 103 +++++++++++++++++++++++++++++-
 applications/gitlab/approvals.py          |  38 +++++++++++
 applications/gitlab/branch_protections.py |  25 ++++++++
 applications/gitlab/gitlab_admins.py      |  25 --------
 applications/gitlab/passwords.py          |  29 +++++++++
 applications/gitlab/provisioning.py       |  32 ++++++++++
 applications/gitlab/users.py              |  53 +++++++++++++++
 7 files changed, 278 insertions(+), 27 deletions(-)

diff --git a/applications/gitlab/README.org b/applications/gitlab/README.org
index 81d610d..37ce59b 100644
--- a/applications/gitlab/README.org
+++ b/applications/gitlab/README.org
@@ -1,11 +1,110 @@
 #+title: GitLab Scripts
 
-* =gitlab_admins.py=
+* =approvals.py=
+
+\*This script requires an active Premium or Ultimate subscription.*\
+
+#+begin_src sh
+python ./approvals.py
+#+end_src
+
+#+begin_src text
+Rule: All Members
+  Approvals Required: 1
+  Rule type: any_approver
+Rule: Default
+  Approvals Required: 1
+  Rule type: regular
+  Protected Branch: master
+  Eligible Approver: Christian Cleberg
+#+end_src
+
+* =branch_protections.py=
+
+#+begin_src sh
+python ./branch_protections.py
+#+end_src
+
+#+begin_src json
+[
+    {
+        "id": 148448212,
+        "name": "main",
+        "push_access_levels": [
+            {
+                "id": 185900194,
+                "access_level": 40,
+                "access_level_description": "Maintainers",
+                "deploy_key_id": null,
+                "user_id": null,
+                "group_id": null
+            }
+        ],
+        "merge_access_levels": [
+            {
+                "id": 156461000,
+                "access_level": 40,
+                "access_level_description": "Maintainers",
+                "user_id": null,
+                "group_id": null
+            }
+        ],
+        "allow_force_push": false,
+        "unprotect_access_levels": [],
+        "code_owner_approval_required": false,
+        "inherited": false
+    }
+]
+#+end_src
+
+* =passwords.py=
+
+\*This script does not apply to GitLab.com. This is for self-hosted instances only.*\
+
+#+begin_src sh
+python ./passwords.py
+#+end_src
+
+#+begin_src text
+# TODO: Need access to a self-hosted version of GitLab to test this out.
+#+end_src
+
+* =provisioning.py=
+
+\*This script requires an active Premium or Ultimate subscription.*\
 
 #+begin_src sh
-python ./gitlab_admins.py
+python ./provisioning.py
 #+end_src
 
 #+begin_src text
+Group: 105300140
+    2025-04-08T03:33:17.055Z : Action: member_created, Member: 128029250, Author: 24608590
+#+end_src
+
+* =users.py=
+
+#+begin_src sh
+python ./users.py
+#+end_src
+
+#+begin_src text
+Access Level Roles:
+    0  : No access
+    5  : Minimal access
+    10 : Guest
+    15 : Planner
+    20 : Reporter
+    30 : Developer
+    40 : Maintainer
+    50 : Owner
+    60 : Admin
+
+
+Group 97083755 Members:
+Username: ccleberg, Access Level: 50
+
+Project 68701468 Members:
 Username: ccleberg, Access Level: 50
+Username: project_68701468_bot_2c7ee010a479c0e48cdb4c7c5cfae886, Access Level: 40
 #+end_src
diff --git a/applications/gitlab/approvals.py b/applications/gitlab/approvals.py
new file mode 100644
index 0000000..b6ea3ef
--- /dev/null
+++ b/applications/gitlab/approvals.py
@@ -0,0 +1,38 @@
+"""

+Extract merge request approval rules and their statuses in GitLab.

+"""

+

+import requests

+

+BASE_URL = "https://gitlab.com/api/v4"

+PRIVATE_TOKEN = "your_access_token"

+PROJECT_ID = "your_project_id"

+TIMEOUT = 30

+

+URL = f"{BASE_URL}/projects/{PROJECT_ID}/approval_rules"

+HEADERS = {"PRIVATE-TOKEN": PRIVATE_TOKEN}

+

+if __name__ == "__main__":

+    # Get approval rules

+    response = requests.get(URL, headers=HEADERS, timeout=TIMEOUT)

+    if response.status_code == 200:

+        approval_rules = response.json()

+        for rule in approval_rules:

+            name = rule["name"]

+            approvals_required = rule["approvals_required"]

+            rule_type = rule["rule_type"]

+            protected_branches = rule["protected_branches"]

+            eligible_approvers = rule["eligible_approvers"]

+            print(f"Rule: {name}")

+            print(f"  Approvals Required: {approvals_required}")

+            print(f"  Rule type: {rule_type}")

+            for branch in protected_branches:

+                branch_name = branch["name"]

+                print(f"  Protected Branch: {branch_name}")

+            for approver in eligible_approvers:

+                approver_username = approver["name"]

+                print(f"  Eligible Approver: {approver_username}")

+    else:

+        print(

+            f"Failed to fetch approval rules: {response.status_code}, {response.text}"

+        )

diff --git a/applications/gitlab/branch_protections.py b/applications/gitlab/branch_protections.py
new file mode 100644
index 0000000..3d83165
--- /dev/null
+++ b/applications/gitlab/branch_protections.py
@@ -0,0 +1,25 @@
+"""

+List all branch protection rules and their configurations in GitLab.

+"""

+

+import requests

+import json

+

+BASE_URL = "https://gitlab.com/api/v4"

+PRIVATE_TOKEN = "your_access_token"

+PROJECT_ID = "your_project_id"

+TIMEOUT = 30

+

+URL = f"{BASE_URL}/projects/{PROJECT_ID}/protected_branches"

+HEADERS = {"PRIVATE-TOKEN": PRIVATE_TOKEN}

+

+if __name__ == "__main__":

+    # Get protected branches

+    response = requests.get(URL, headers=HEADERS, timeout=TIMEOUT)

+    if response.status_code == 200:

+        protected_branches = response.json()

+        print(json.dumps(protected_branches, indent=4))

+    else:

+        print(

+            f"Failed to fetch protected branches: {response.status_code}, {response.text}"

+        )

diff --git a/applications/gitlab/gitlab_admins.py b/applications/gitlab/gitlab_admins.py
deleted file mode 100644
index 091032c..0000000
--- a/applications/gitlab/gitlab_admins.py
+++ /dev/null
@@ -1,25 +0,0 @@
-"""
-Gather all members of a GitLab group and their access levels.
-"""
-
-import requests
-
-BASE_URL = "https://gitlab.com/api/v4"
-PRIVATE_TOKEN = "your_access_token"
-GROUP_ID = "your_group_id"
-TIMEOUT = 30
-
-URL = f"{BASE_URL}/groups/{GROUP_ID}/members"
-HEADERS = {"PRIVATE-TOKEN": PRIVATE_TOKEN}
-
-if __name__ == "__main__":
-    # Get group members
-    response = requests.get(URL, headers=HEADERS, timeout=TIMEOUT)
-    if response.status_code == 200:
-        members = response.json()
-        for member in members:
-            print(
-                f"Username: {member['username']}, Access Level: {member['access_level']}"
-            )
-    else:
-        print(f"Failed to fetch group members: {response.status_code}, {response.text}")
diff --git a/applications/gitlab/passwords.py b/applications/gitlab/passwords.py
new file mode 100644
index 0000000..84ab107
--- /dev/null
+++ b/applications/gitlab/passwords.py
@@ -0,0 +1,29 @@
+"""

+Verify if password policies are enforced in a self-hosted GitLab instance.

+

+    Ref: https://docs.gitlab.com/api/settings/

+"""

+

+import requests

+

+BASE_URL = "https://gitlab.com/api/v4"

+PRIVATE_TOKEN = "your_access_token"

+TIMEOUT = 30

+

+URL = f"{BASE_URL}/application/settings"

+HEADERS = {"PRIVATE-TOKEN": PRIVATE_TOKEN}

+

+if __name__ == "__main__":

+    # Get application settings

+    response = requests.get(URL, headers=HEADERS, timeout=TIMEOUT)

+    if response.status_code == 200:

+        settings = response.json()

+        password_length = settings.get("password_length", "Not set")

+        password_complexity = settings.get("password_complexity", "Not set")

+

+        print(f"Password Length: {password_length}")

+        print(f"Password Complexity: {password_complexity}")

+    else:

+        print(

+            f"Failed to fetch application settings: {response.status_code}, {response.text}"

+        )

diff --git a/applications/gitlab/provisioning.py b/applications/gitlab/provisioning.py
new file mode 100644
index 0000000..bd0e695
--- /dev/null
+++ b/applications/gitlab/provisioning.py
@@ -0,0 +1,32 @@
+"""

+Track user creation and deletion events in GitLab with timestamps.

+"""

+

+import requests

+

+BASE_URL = "https://gitlab.com/api/v4"

+PRIVATE_TOKEN = "your_access_token"

+GROUP_ID = "your_group_id"

+TIMEOUT = 30

+

+URL = f"{BASE_URL}/groups/{GROUP_ID}/audit_events"

+HEADERS = {"PRIVATE-TOKEN": PRIVATE_TOKEN}

+

+if __name__ == "__main__":

+    # Get audit events

+    response = requests.get(URL, headers=HEADERS, timeout=TIMEOUT)

+    if response.status_code == 200:

+        audit_events = response.json()

+        for event in audit_events:

+            if event["entity_type"] == "User" or event["entity_type"] == "Group":

+                action = event["event_name"]

+                member_id = event["details"].get("member_id")

+                created_at = event["created_at"]

+                author = event["author_id"]

+                if action in ["member_created", "member_destroyed", "member_updated"]:

+                    print(

+                        f"Group: {GROUP_ID}\n",

+                        f"   {created_at} : Action: {action}, Member: {member_id}, Author: {author}",

+                    )

+    else:

+        print(f"Failed to fetch audit events: {response.status_code}, {response.text}")

diff --git a/applications/gitlab/users.py b/applications/gitlab/users.py
new file mode 100644
index 0000000..fab2646
--- /dev/null
+++ b/applications/gitlab/users.py
@@ -0,0 +1,53 @@
+"""
+Gather all members of specified GitLab groups and projects and their access levels.
+
+        Ref: https://docs.gitlab.com/api/members/
+"""
+
+import requests
+
+BASE_URL = "https://gitlab.com/api/v4"
+PRIVATE_TOKEN = "your_access_token"
+GROUP_IDS = ["group_id_1", "group_id_2"]  # Add your group IDs here
+PROJECT_IDS = ["project_id_1", "project_id_2"]  # Add your project IDs here
+TIMEOUT = 30
+
+HEADERS = {"PRIVATE-TOKEN": PRIVATE_TOKEN}
+
+
+def get_members(url, name):
+    response = requests.get(url, headers=HEADERS, timeout=TIMEOUT)
+    if response.status_code == 200:
+        members = response.json()
+        print(f"\n{name} Members:")
+        for member in members:
+            print(
+                f"Username: {member['username']}, Access Level: {member['access_level']}"
+            )
+    else:
+        print(
+            f"Failed to fetch members for {name}: {response.status_code}, {response.text}"
+        )
+
+
+if __name__ == "__main__":
+    access_levels = """Access Level Roles:
+    0  : No access
+    5  : Minimal access
+    10 : Guest
+    15 : Planner
+    20 : Reporter
+    30 : Developer
+    40 : Maintainer
+    50 : Owner
+    60 : Admin
+    """
+    print(access_levels)
+
+    for group_id in GROUP_IDS:
+        group_url = f"{BASE_URL}/groups/{group_id}/members"
+        get_members(group_url, f"Group {group_id}")
+
+    for project_id in PROJECT_IDS:
+        project_url = f"{BASE_URL}/projects/{project_id}/members"
+        get_members(project_url, f"Project {project_id}")