audit-labs/audit-report

Turn audit-tools evidence packages into control-mapped, auditor-ready reports. audit compliance evidence reporting https://audit-labs.dev/audit-report/

Commit f14a20bc6c

f14a20bc6c364cb7e2758ab3ff60c1532fb995c4

parent: 217d893956

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-06 07:14 UTC

feat: add GitLab ruleset

Six checks grounded in the real audit-tools GitLab schemas: force push on
protected branches, code-owner approval, approval-rule strength, public project
visibility, instance password policy, and audit-log activity. Handles GitLab's
per-branch protection model and its omission of empty CSVs (absent table reports
as not applicable, not pass). Adds a fixture package and engine tests.

Layout: unified · split

README.md +1
@@ -71,6 +71,7 @@ python -m audit_report ./output/aws_audit_default_2026-07-29
7171| --- | --- | --- |
7272| AWS | `aws_audit_*` | Console-user MFA, access-key rotation, root MFA & keys, password policy, open SSH, public S3, CloudTrail logging |
7373| GitHub | `github_audit_*` | Org 2FA, base permission, secret-scanning push protection, default-branch protection, required reviews |
74| GitLab | `gitlab_audit_*` | Force-push on protected branches, code-owner approval, approval-rule strength, public projects, instance password policy, audit logging |
7475
7576## Writing your own rules
7677
audit_report/rulesets/gitlab.yaml added +101
@@ -0,0 +1,101 @@
1# Ruleset: GitLab
2#
3# Evaluated against an audit-tools GitLab evidence package
4# (gitlab_audit_<group>_<date>/). Note two GitLab-specific facts that shape
5# these rules:
6#
7# * branch_protections lists one row *per protected branch* — there is no
8# "protected" boolean as on GitHub. A row's existence means the branch is
9# protected; the checks below test how strong that protection is.
10# * audit-tools omits a CSV entirely when a collector returns no rows, so an
11# absent table reports as "not applicable", not "pass". A rule can only
12# speak to data that was actually collected.
13platform: gitlab
14
15rules:
16 - id: gitlab.branch.no-force-push
17 title: Protected branches disallow force push
18 table: branch_protections
19 severity: high
20 controls: [SOC2:CC8.1, ISO:A.8.32, NIST:CM-6]
21 rationale: >
22 Allowing force push to a protected branch lets history be rewritten with
23 no trace, defeating the change-management record the protection exists to
24 provide.
25 remediation: Disable "Allow force push" on protected branches.
26 check:
27 type: fail_rows_where
28 when: {column: allow_force_push, op: is_true}
29
30 - id: gitlab.branch.code-owner-approval
31 title: Protected branches require code owner approval
32 table: branch_protections
33 severity: low
34 controls: [SOC2:CC8.1, ISO:A.8.32]
35 rationale: >
36 Code owner approval routes changes to the people accountable for the
37 affected files. It is a stricter posture than a plain review requirement
38 and is not needed everywhere — hence low severity.
39 remediation: Enable "Require approval from code owners" on protected branches where ownership matters.
40 check:
41 type: fail_rows_where
42 when: {column: code_owner_approval_required, op: is_false}
43
44 - id: gitlab.approvals.require-one
45 title: Approval rules require at least one approval
46 table: approval_rules
47 severity: medium
48 controls: [SOC2:CC8.1, ISO:A.8.32, NIST:CM-6]
49 rationale: >
50 An approval rule that requires zero approvals contributes nothing to
51 segregation of duties — a change can merge with no second person signing
52 off.
53 remediation: Set the required number of approvals to at least one on merge-blocking rules.
54 check:
55 type: fail_rows_where
56 when: {column: approvals_required, op: lt, value: 1}
57
58 - id: gitlab.projects.no-public
59 title: Projects are not publicly visible
60 table: projects
61 severity: medium
62 controls: [SOC2:CC6.3, ISO:A.5.15, NIST:AC-6]
63 rationale: >
64 A public project exposes its source and history to anyone on the
65 internet. That is sometimes intended (open source) but should be a
66 deliberate, reviewed decision rather than a default.
67 remediation: Set project visibility to private or internal unless public exposure is intended and approved.
68 check:
69 type: fail_rows_where
70 when: {column: visibility, op: equals, value: public}
71
72 - id: gitlab.password-policy
73 title: Instance password policy meets baseline strength
74 table: password_policy
75 severity: medium
76 controls: [ISO:A.5.17, NIST:IA-5]
77 rationale: >
78 A weak password policy undermines every password-based control. This table
79 is only present for self-hosted instances audited with an admin token; on
80 GitLab.com it is absent and this rule reports as not applicable.
81 remediation: Require a minimum length of 12 and mandate numbers and symbols in the instance settings.
82 check:
83 type: assert_row
84 require:
85 - {column: minimum_password_length, op: gte, value: 12}
86 - {column: password_number_required, op: is_true}
87 - {column: password_symbol_required, op: is_true}
88
89 - id: gitlab.audit.logging-active
90 title: Audit event logging is producing records
91 table: audit_events
92 severity: medium
93 controls: [SOC2:CC7.2, ISO:A.8.15, NIST:AU-2]
94 rationale: >
95 The presence of audit events is direct evidence that GitLab is recording
96 security-relevant activity. Absence here means no events were collected —
97 reported as not applicable rather than as a pass or a failure.
98 remediation: Confirm audit events are enabled and retained for the group and its projects.
99 check:
100 type: require_any_row
101 when: {column: action, op: not_empty}
tests/fixtures/gitlab_audit_acme_2026-01-01/approval_rules.csv added +3
@@ -0,0 +1,3 @@
1project,rule,rule_type,approvals_required,protected_branches,eligible_approvers
2acme/app,Default,regular,1,main,alice
3acme/site,License-Check,regular,0,(all),(none)
tests/fixtures/gitlab_audit_acme_2026-01-01/audit_events.csv added +2
@@ -0,0 +1,2 @@
1created_at,action,member_id,target,author_id,entity_type
22026-01-01T00:00:00Z,add_user,42,bob,1,Group
tests/fixtures/gitlab_audit_acme_2026-01-01/branch_protections.csv added +3
@@ -0,0 +1,3 @@
1project,branch,push_access,merge_access,allow_force_push,code_owner_approval_required
2acme/app,main,No one,Maintainer,False,True
3acme/site,main,Maintainer,Developer,True,False
tests/fixtures/gitlab_audit_acme_2026-01-01/group_members.csv added +3
@@ -0,0 +1,3 @@
1username,name,access_level,role,state
2alice,Alice,50,Owner,active
3bob,Bob,30,Developer,active
tests/fixtures/gitlab_audit_acme_2026-01-01/projects.csv added +3
@@ -0,0 +1,3 @@
1id,name,path,visibility,default_branch,archived,web_url
21,app,acme/app,private,main,False,https://gitlab.com/acme/app
32,site,acme/site,public,main,False,https://gitlab.com/acme/site
tests/test_engine.py +26
@@ -70,6 +70,32 @@ def test_github_fixture_findings():
7070 assert findings["github.branch.require-reviews"].status == PASS
7171
7272
73def test_gitlab_fixture_findings():
74 findings = _run("gitlab_audit_acme_2026-01-01", "gitlab.yaml")
75
76 # 'site' allows force push on a protected branch.
77 force = findings["gitlab.branch.no-force-push"]
78 assert force.status == FAIL
79 assert force.evidence[0]["project"] == "acme/site"
80
81 # 'site' does not require code owner approval.
82 assert findings["gitlab.branch.code-owner-approval"].status == FAIL
83
84 # The License-Check rule requires zero approvals.
85 approvals = findings["gitlab.approvals.require-one"]
86 assert approvals.status == FAIL
87 assert approvals.evidence[0]["rule"] == "License-Check"
88
89 # 'site' is a public project.
90 assert findings["gitlab.projects.no-public"].status == FAIL
91
92 # No password_policy.csv in the package (as on GitLab.com) -> not applicable.
93 assert findings["gitlab.password-policy"].status == NOT_APPLICABLE
94
95 # An audit event with an action is present -> logging is evidenced.
96 assert findings["gitlab.audit.logging-active"].status == PASS
97
98
7399def test_not_applicable_when_table_absent(tmp_path):
74100 (tmp_path / "aws_audit_x_2026-01-01").mkdir()
75101 pkg_dir = tmp_path / "aws_audit_x_2026-01-01"