commitguardian 0.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.
Files changed (197) hide show
  1. commitguard/__init__.py +26 -0
  2. commitguard/__main__.py +6 -0
  3. commitguard/api/__init__.py +18 -0
  4. commitguard/api/app.py +1376 -0
  5. commitguard/api/governance.py +1085 -0
  6. commitguard/api/hosting.py +196 -0
  7. commitguard/api/http.py +252 -0
  8. commitguard/api/settings.py +169 -0
  9. commitguard/audit/__init__.py +13 -0
  10. commitguard/audit/logger.py +34 -0
  11. commitguard/audit/models.py +222 -0
  12. commitguard/audit/storage.py +59 -0
  13. commitguard/ci/__init__.py +7 -0
  14. commitguard/ci/context.py +60 -0
  15. commitguard/cli/__init__.py +6 -0
  16. commitguard/cli/app.py +74 -0
  17. commitguard/cli/commands/__init__.py +1 -0
  18. commitguard/cli/commands/benchmark.py +441 -0
  19. commitguard/cli/commands/check.py +100 -0
  20. commitguard/cli/commands/ci.py +165 -0
  21. commitguard/cli/commands/dashboard.py +141 -0
  22. commitguard/cli/commands/doctor.py +533 -0
  23. commitguard/cli/commands/github.py +449 -0
  24. commitguard/cli/commands/hook.py +156 -0
  25. commitguard/cli/commands/init.py +137 -0
  26. commitguard/cli/commands/install.py +152 -0
  27. commitguard/cli/commands/policy.py +36 -0
  28. commitguard/cli/commands/report.py +39 -0
  29. commitguard/cli/commands/reproduce.py +123 -0
  30. commitguard/cli/commands/scan.py +47 -0
  31. commitguard/cli/common.py +44 -0
  32. commitguard/cli/output.py +89 -0
  33. commitguard/cli/render.py +367 -0
  34. commitguard/config/__init__.py +6 -0
  35. commitguard/config/defaults.py +53 -0
  36. commitguard/config/enforcement.py +53 -0
  37. commitguard/config/loader.py +174 -0
  38. commitguard/config/schema.py +105 -0
  39. commitguard/config/sources.py +183 -0
  40. commitguard/controlplane/__init__.py +24 -0
  41. commitguard/controlplane/access.py +231 -0
  42. commitguard/controlplane/commands.py +393 -0
  43. commitguard/controlplane/errors.py +88 -0
  44. commitguard/controlplane/identity.py +478 -0
  45. commitguard/controlplane/members.py +219 -0
  46. commitguard/controlplane/notifications.py +787 -0
  47. commitguard/controlplane/pagination.py +146 -0
  48. commitguard/controlplane/policies.py +1204 -0
  49. commitguard/controlplane/queries.py +1814 -0
  50. commitguard/controlplane/results.py +909 -0
  51. commitguard/controlplane/rules.py +184 -0
  52. commitguard/controlplane/views.py +799 -0
  53. commitguard/core/__init__.py +6 -0
  54. commitguard/core/context.py +31 -0
  55. commitguard/core/decision.py +58 -0
  56. commitguard/core/engine.py +82 -0
  57. commitguard/core/result.py +177 -0
  58. commitguard/detectors/__init__.py +6 -0
  59. commitguard/detectors/base.py +58 -0
  60. commitguard/detectors/bot.py +87 -0
  61. commitguard/detectors/coauthor.py +86 -0
  62. commitguard/detectors/identity.py +76 -0
  63. commitguard/detectors/registry.py +72 -0
  64. commitguard/detectors/trailer.py +211 -0
  65. commitguard/exceptions/__init__.py +33 -0
  66. commitguard/exceptions/base.py +9 -0
  67. commitguard/exceptions/configuration.py +22 -0
  68. commitguard/exceptions/detection.py +11 -0
  69. commitguard/exceptions/git.py +41 -0
  70. commitguard/exceptions/service.py +25 -0
  71. commitguard/git/__init__.py +12 -0
  72. commitguard/git/commands.py +101 -0
  73. commitguard/git/commit.py +97 -0
  74. commitguard/git/diff.py +36 -0
  75. commitguard/git/hooks.py +527 -0
  76. commitguard/git/push.py +93 -0
  77. commitguard/git/ranges.py +71 -0
  78. commitguard/git/repository.py +447 -0
  79. commitguard/github/__init__.py +34 -0
  80. commitguard/github/actions.py +163 -0
  81. commitguard/github/app.py +935 -0
  82. commitguard/github/auth.py +217 -0
  83. commitguard/github/check_runs.py +172 -0
  84. commitguard/github/checks.py +210 -0
  85. commitguard/github/client.py +844 -0
  86. commitguard/github/enforcement_status.py +209 -0
  87. commitguard/github/errors.py +129 -0
  88. commitguard/github/events.py +563 -0
  89. commitguard/github/identifiers.py +90 -0
  90. commitguard/github/installations.py +566 -0
  91. commitguard/github/markdown.py +19 -0
  92. commitguard/github/permissions.py +70 -0
  93. commitguard/github/pull_requests.py +53 -0
  94. commitguard/github/queue.py +47 -0
  95. commitguard/github/recovery.py +124 -0
  96. commitguard/github/repositories.py +305 -0
  97. commitguard/github/server.py +52 -0
  98. commitguard/github/settings.py +174 -0
  99. commitguard/github/storage.py +2315 -0
  100. commitguard/github/webhooks.py +129 -0
  101. commitguard/github/worker.py +628 -0
  102. commitguard/github/workflow.py +286 -0
  103. commitguard/governance/__init__.py +26 -0
  104. commitguard/governance/bulk.py +765 -0
  105. commitguard/governance/cache.py +88 -0
  106. commitguard/governance/common.py +216 -0
  107. commitguard/governance/exceptions.py +861 -0
  108. commitguard/governance/groups.py +448 -0
  109. commitguard/governance/inventory.py +386 -0
  110. commitguard/governance/posture.py +1272 -0
  111. commitguard/governance/resolver.py +632 -0
  112. commitguard/governance/rollouts.py +760 -0
  113. commitguard/governance/rules.py +371 -0
  114. commitguard/governance/schedules.py +663 -0
  115. commitguard/governance/service.py +120 -0
  116. commitguard/governance/settings.py +365 -0
  117. commitguard/governance/simulation.py +618 -0
  118. commitguard/governance/workflow.py +734 -0
  119. commitguard/notifications/__init__.py +2 -0
  120. commitguard/notifications/channels/__init__.py +1 -0
  121. commitguard/notifications/channels/base.py +22 -0
  122. commitguard/notifications/channels/email.py +110 -0
  123. commitguard/notifications/channels/in_app.py +74 -0
  124. commitguard/notifications/channels/sink.py +58 -0
  125. commitguard/notifications/channels/webhook.py +233 -0
  126. commitguard/notifications/deduplication.py +57 -0
  127. commitguard/notifications/dispatcher.py +201 -0
  128. commitguard/notifications/models.py +439 -0
  129. commitguard/notifications/outbox.py +106 -0
  130. commitguard/notifications/preferences.py +224 -0
  131. commitguard/notifications/retry.py +282 -0
  132. commitguard/notifications/service.py +128 -0
  133. commitguard/notifications/settings.py +167 -0
  134. commitguard/notifications/templates.py +108 -0
  135. commitguard/observability/__init__.py +5 -0
  136. commitguard/observability/logging.py +161 -0
  137. commitguard/observability/metrics.py +105 -0
  138. commitguard/policies/__init__.py +6 -0
  139. commitguard/policies/defaults.py +48 -0
  140. commitguard/policies/evaluator.py +66 -0
  141. commitguard/policies/governance.py +498 -0
  142. commitguard/policies/loader.py +23 -0
  143. commitguard/policies/mandatory.py +52 -0
  144. commitguard/policies/model.py +46 -0
  145. commitguard/provenance/__init__.py +9 -0
  146. commitguard/provenance/author.py +146 -0
  147. commitguard/provenance/committer.py +16 -0
  148. commitguard/provenance/normalization.py +158 -0
  149. commitguard/provenance/signatures.py +34 -0
  150. commitguard/provenance/trailers.py +256 -0
  151. commitguard/research/__init__.py +26 -0
  152. commitguard/research/compare.py +231 -0
  153. commitguard/research/datasets.py +1484 -0
  154. commitguard/research/detection.py +183 -0
  155. commitguard/research/environment.py +185 -0
  156. commitguard/research/gitenv.py +108 -0
  157. commitguard/research/hooks.py +247 -0
  158. commitguard/research/metrics.py +85 -0
  159. commitguard/research/performance.py +194 -0
  160. commitguard/research/platform.py +288 -0
  161. commitguard/research/report.py +372 -0
  162. commitguard/research/repository.py +111 -0
  163. commitguard/research/reproduction.py +297 -0
  164. commitguard/research/results.py +94 -0
  165. commitguard/rules/__init__.py +11 -0
  166. commitguard/rules/data/ai-domains.yaml +51 -0
  167. commitguard/rules/data/ai-identities.yaml +131 -0
  168. commitguard/rules/data/bot-identities.yaml +53 -0
  169. commitguard/rules/data/patterns.yaml +52 -0
  170. commitguard/rules/loader.py +102 -0
  171. commitguard/rules/matcher.py +212 -0
  172. commitguard/rules/models.py +269 -0
  173. commitguard/security/__init__.py +5 -0
  174. commitguard/security/hashing.py +30 -0
  175. commitguard/security/rate_limit.py +33 -0
  176. commitguard/security/safe_yaml.py +69 -0
  177. commitguard/security/sanitization.py +85 -0
  178. commitguard/security/secrets.py +169 -0
  179. commitguard/security/validation.py +89 -0
  180. commitguard/services/__init__.py +15 -0
  181. commitguard/services/analysis.py +119 -0
  182. commitguard/services/audit.py +95 -0
  183. commitguard/services/ci.py +383 -0
  184. commitguard/services/enforcement.py +102 -0
  185. commitguard/services/hooks.py +254 -0
  186. commitguard/services/remediation.py +99 -0
  187. commitguard/services/reports.py +146 -0
  188. commitguard/services/scan.py +172 -0
  189. commitguard/utils/__init__.py +1 -0
  190. commitguard/utils/filesystem.py +72 -0
  191. commitguard/utils/platform.py +35 -0
  192. commitguard/utils/subprocess.py +84 -0
  193. commitguardian-0.1.0.dist-info/METADATA +694 -0
  194. commitguardian-0.1.0.dist-info/RECORD +197 -0
  195. commitguardian-0.1.0.dist-info/WHEEL +4 -0
  196. commitguardian-0.1.0.dist-info/entry_points.txt +2 -0
  197. commitguardian-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,165 @@
1
+ """``commitguard ci github``: server-side enforcement inside GitHub Actions.
2
+
3
+ Reads ``GITHUB_EVENT_NAME`` / ``GITHUB_EVENT_PATH``, analyses the commits the
4
+ event introduces with the policy from a trusted commit, and exits:
5
+
6
+ * 0 - allowed (warnings are reported but pass unless ``--fail-on warn``);
7
+ * 1 - blocked by policy;
8
+ * 2 - CommitGuard could not complete the check (fails the job: fail closed).
9
+
10
+ No token, secret, network access or write permission is used.
11
+ """
12
+
13
+ import os
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import Annotated
17
+
18
+ import typer
19
+
20
+ from commitguard.cli.output import ExitCode, OutputFormat, error
21
+ from commitguard.cli.render import render_ci_text, render_json
22
+ from commitguard.core.decision import Action
23
+ from commitguard.git.repository import Repository
24
+ from commitguard.github.actions import (
25
+ annotation_commands,
26
+ append_file,
27
+ commands_stopped,
28
+ render_step_summary,
29
+ running_in_github_actions,
30
+ step_outputs,
31
+ workflow_command,
32
+ )
33
+ from commitguard.github.checks import build_check_output
34
+ from commitguard.github.events import load_github_event
35
+ from commitguard.security.sanitization import sanitize_block
36
+ from commitguard.security.validation import validate_repository_path
37
+ from commitguard.services.ci import DEFAULT_CI_MAX_COMMITS
38
+ from commitguard.services.scan import ScanRequest, ScanService
39
+ from commitguard.utils.filesystem import atomic_write_text
40
+
41
+ ci_app = typer.Typer(help="Server-side enforcement in CI systems.", no_args_is_help=True)
42
+
43
+
44
+ def _emit(line: str) -> None:
45
+ sys.stdout.write(line + "\n")
46
+ sys.stdout.flush()
47
+
48
+
49
+ def _env_path(name: str) -> Path | None:
50
+ value = os.environ.get(name)
51
+ return Path(value) if value else None
52
+
53
+
54
+ @ci_app.command("github")
55
+ def github_command(
56
+ event_name: Annotated[
57
+ str | None,
58
+ typer.Option("--event-name", help="Event name. Default: $GITHUB_EVENT_NAME."),
59
+ ] = None,
60
+ event_path: Annotated[
61
+ Path | None,
62
+ typer.Option("--event-path", help="Event payload JSON. Default: $GITHUB_EVENT_PATH."),
63
+ ] = None,
64
+ config: Annotated[
65
+ str | None,
66
+ typer.Option(
67
+ "--config",
68
+ help="Repository-relative policy file, read from the TRUSTED commit "
69
+ "(never from the checked-out change). Default: .commitguard.yaml/.yml.",
70
+ ),
71
+ ] = None,
72
+ fail_on: Annotated[
73
+ Action,
74
+ typer.Option("--fail-on", help="Fail the check at this action or above: block or warn."),
75
+ ] = Action.BLOCK,
76
+ max_commits: Annotated[
77
+ int, typer.Option("--max-commits", min=1, help="Fail if more commits would be analysed.")
78
+ ] = DEFAULT_CI_MAX_COMMITS,
79
+ output_format: Annotated[
80
+ OutputFormat, typer.Option("--format", "-f", case_sensitive=False)
81
+ ] = OutputFormat.TEXT,
82
+ report_file: Annotated[
83
+ Path | None, typer.Option("--report-file", help="Also write the JSON report here.")
84
+ ] = None,
85
+ github_output: Annotated[
86
+ bool,
87
+ typer.Option(
88
+ "--github-output/--no-github-output",
89
+ help="Write annotations, job summary and step outputs when running in GitHub Actions.",
90
+ ),
91
+ ] = True,
92
+ ) -> None:
93
+ """Check the commits a pull request, merge group or push introduces."""
94
+ in_actions = github_output and running_in_github_actions()
95
+ try:
96
+ if fail_on is Action.ALLOW:
97
+ raise ValueError("--fail-on must be block or warn")
98
+ if config is not None:
99
+ validate_repository_path(config)
100
+ context = load_github_event(
101
+ event_name or os.environ.get("GITHUB_EVENT_NAME"),
102
+ event_path or _env_path("GITHUB_EVENT_PATH"),
103
+ )
104
+ result = ScanService().run(
105
+ ScanRequest(
106
+ repository=Repository.discover(),
107
+ context=context,
108
+ config_path=config,
109
+ max_commits=max_commits,
110
+ fail_on=fail_on,
111
+ )
112
+ )
113
+ except Exception as exc: # noqa: BLE001 - any failure must fail the check
114
+ message = f"{type(exc).__name__}: {exc}" if not str(exc) else str(exc)
115
+ if in_actions:
116
+ _emit(
117
+ workflow_command(
118
+ "error",
119
+ f"Security validation could not be completed: {message}",
120
+ title="CommitGuard could not verify repository policy",
121
+ )
122
+ )
123
+ text = (
124
+ "CommitGuard could not verify repository policy.\n"
125
+ f"Reason: {sanitize_block(message, max_length=4000)}\n"
126
+ "Security validation could not be completed.\n"
127
+ "Result: FAILED"
128
+ )
129
+ if in_actions:
130
+ # The reason can quote untrusted YAML or payload text: print it to stdout
131
+ # with workflow commands disabled (stdout/stderr ordering is not guaranteed).
132
+ with commands_stopped(_emit):
133
+ _emit(text)
134
+ else:
135
+ error(text)
136
+ raise typer.Exit(code=int(ExitCode.ERROR)) from None
137
+
138
+ report = result.report
139
+ check = build_check_output(report, fail_on=fail_on)
140
+ failed = not result.enforcement.allowed
141
+
142
+ if output_format is OutputFormat.JSON:
143
+ _emit(render_json(report))
144
+ elif in_actions:
145
+ with commands_stopped(_emit):
146
+ _emit(render_ci_text(report, failed=failed))
147
+ else:
148
+ _emit(render_ci_text(report, failed=failed))
149
+
150
+ if report_file is not None:
151
+ atomic_write_text(report_file, render_json(report) + "\n", overwrite=True)
152
+
153
+ if in_actions:
154
+ if output_format is not OutputFormat.JSON:
155
+ for line in annotation_commands(check):
156
+ _emit(line)
157
+ summary = _env_path("GITHUB_STEP_SUMMARY")
158
+ if summary is not None:
159
+ append_file(summary, render_step_summary(report, check))
160
+ outputs = _env_path("GITHUB_OUTPUT")
161
+ if outputs is not None:
162
+ append_file(outputs, step_outputs(report, check))
163
+
164
+ if failed:
165
+ raise typer.Exit(code=int(ExitCode.BLOCKED))
@@ -0,0 +1,141 @@
1
+ """``commitguard dashboard``: operator tasks for the dashboard control plane.
2
+
3
+ * ``members list`` - roles in an organization;
4
+ * ``members grant`` - grant or change a role (bootstraps the first owner);
5
+ * ``members revoke`` - remove a role.
6
+
7
+ The commands work on the service's state database (``COMMITGUARD_APP_DATA_DIR``)
8
+ and need no network access. Users are named by numeric GitHub user ID (find it
9
+ with ``gh api users/<login> --jq .id``): logins can be renamed and re-registered.
10
+ Every change is recorded as an audit event with the actor ``commitguard-cli``.
11
+ """
12
+
13
+ import os
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING, Annotated
16
+
17
+ import typer
18
+
19
+ from commitguard.cli.output import fail, handled_errors, info
20
+ from commitguard.exceptions.base import CommitGuardError
21
+
22
+ if TYPE_CHECKING: # imported lazily: the CLI must not load sqlite3 for unrelated commands
23
+ from commitguard.controlplane.members import MembershipService
24
+ from commitguard.github.storage import SqliteStateStore
25
+
26
+ dashboard_app = typer.Typer(
27
+ help="Dashboard control plane: organization members and roles.", no_args_is_help=True
28
+ )
29
+ members_app = typer.Typer(help="Organization members and their roles.", no_args_is_help=True)
30
+ dashboard_app.add_typer(members_app, name="members")
31
+
32
+ CLI_ACTOR_LOGIN = "commitguard-cli"
33
+ _ROLES = ("viewer", "security_manager", "admin", "owner")
34
+
35
+
36
+ def _services() -> "tuple[SqliteStateStore, MembershipService]":
37
+ from commitguard.controlplane.members import MembershipService
38
+ from commitguard.github.settings import ENV_DATA_DIR
39
+ from commitguard.github.storage import DATABASE_FILENAME, SqliteStateStore
40
+ from commitguard.services.audit import AuditService
41
+
42
+ raw = os.environ.get(ENV_DATA_DIR)
43
+ if not raw or not Path(raw).is_absolute():
44
+ raise CommitGuardError(
45
+ f"{ENV_DATA_DIR} must be set to the service's absolute data directory"
46
+ )
47
+ database = Path(raw) / DATABASE_FILENAME
48
+ if not database.is_file():
49
+ raise CommitGuardError(f"no CommitGuard state database in {ENV_DATA_DIR}")
50
+ store = SqliteStateStore(database)
51
+ audit = AuditService([store])
52
+ return store, MembershipService(store, audit)
53
+
54
+
55
+ def _organization(members: "MembershipService", value: str) -> tuple[int, str, str]:
56
+ account = (
57
+ members.account(int(value))
58
+ if value.isascii() and value.isdigit()
59
+ else members.account_by_login(value)
60
+ )
61
+ if account is None:
62
+ raise CommitGuardError(
63
+ f"no GitHub App installation is known for organization {value!r} "
64
+ "(install the App, or let the service receive its installation event first)"
65
+ )
66
+ return account
67
+
68
+
69
+ OrganizationOption = Annotated[
70
+ str, typer.Option("--organization", help="Organization login or numeric account ID.")
71
+ ]
72
+ UserIdOption = Annotated[int, typer.Option("--user-id", min=1, help="Numeric GitHub user ID.")]
73
+
74
+
75
+ @members_app.command("list")
76
+ def list_command(organization: OrganizationOption) -> None:
77
+ """List the members of an organization."""
78
+ with handled_errors():
79
+ store, members = _services()
80
+ try:
81
+ account = _organization(members, organization)
82
+ rows = members.list_members(account[0], limit=1000)
83
+ finally:
84
+ store.close()
85
+ info(f"{account[1]} ({account[2]}, account {account[0]})")
86
+ if not rows:
87
+ info(" no members")
88
+ for member in rows:
89
+ label = member.login or "(not signed in yet)"
90
+ suffix = " [implicit]" if member.implicit else ""
91
+ info(f" {member.user_id:<12} {label:<40} {member.role}{suffix}")
92
+
93
+
94
+ @members_app.command("grant")
95
+ def grant_command(
96
+ organization: OrganizationOption,
97
+ user_id: UserIdOption,
98
+ role: Annotated[str, typer.Option("--role", help="viewer, security_manager, admin or owner.")],
99
+ login: Annotated[
100
+ str | None, typer.Option("--login", help="GitHub login, for display until they sign in.")
101
+ ] = None,
102
+ ) -> None:
103
+ """Grant a role (or change an existing one)."""
104
+ from commitguard.audit.models import Actor, ActorType
105
+ from commitguard.controlplane.access import Role
106
+
107
+ if role not in _ROLES:
108
+ fail(f"--role must be one of: {', '.join(_ROLES)}")
109
+ with handled_errors():
110
+ store, members = _services()
111
+ try:
112
+ account = _organization(members, organization)
113
+ member = members.grant(
114
+ account_id=account[0],
115
+ user_id=user_id,
116
+ role=Role(role),
117
+ actor=Actor(type=ActorType.SYSTEM, login=CLI_ACTOR_LOGIN),
118
+ login=login,
119
+ )
120
+ finally:
121
+ store.close()
122
+ info(f"Granted {member.role} in {account[1]} to user {member.user_id}.")
123
+
124
+
125
+ @members_app.command("revoke")
126
+ def revoke_command(organization: OrganizationOption, user_id: UserIdOption) -> None:
127
+ """Remove a member's role."""
128
+ from commitguard.audit.models import Actor, ActorType
129
+
130
+ with handled_errors():
131
+ store, members = _services()
132
+ try:
133
+ account = _organization(members, organization)
134
+ members.remove(
135
+ account_id=account[0],
136
+ user_id=user_id,
137
+ actor=Actor(type=ActorType.SYSTEM, login=CLI_ACTOR_LOGIN),
138
+ )
139
+ finally:
140
+ store.close()
141
+ info(f"Removed user {user_id} from {account[1]}.")