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,174 @@
1
+ """GitHub App configuration from the environment.
2
+
3
+ ===================================================== ==========================================
4
+ Variable Meaning
5
+ ===================================================== ==========================================
6
+ ``COMMITGUARD_GITHUB_APP_ID`` numeric App ID (JWT issuer)
7
+ ``COMMITGUARD_GITHUB_PRIVATE_KEY`` PEM private key text, *or*
8
+ ``COMMITGUARD_GITHUB_PRIVATE_KEY_FILE`` path to the PEM file (preferred)
9
+ ``COMMITGUARD_GITHUB_WEBHOOK_SECRET`` webhook secret, *or*
10
+ ``COMMITGUARD_GITHUB_WEBHOOK_SECRET_FILE`` path to a file containing it
11
+ ``COMMITGUARD_APP_DATA_DIR`` state database and metadata mirrors
12
+ ``COMMITGUARD_APP_MANDATORY_POLICY_FILE`` optional mandatory (organisation) policy
13
+ ``COMMITGUARD_APP_WORKERS`` scan worker threads (default 2)
14
+ ``COMMITGUARD_APP_RETENTION_DAYS`` retention for deliveries/scans/audit (30)
15
+ ``COMMITGUARD_APP_MAX_COMMITS`` per-scan commit limit (10000)
16
+ ===================================================== ==========================================
17
+
18
+ A client ID/secret is not needed: the App never acts on behalf of a user.
19
+
20
+ Errors name the variable, never its value. Secret values are wrapped in
21
+ :class:`~commitguard.security.secrets.Secret` and registered for redaction the
22
+ moment they are read.
23
+ """
24
+
25
+ import os
26
+ import stat
27
+ from collections.abc import Mapping
28
+ from dataclasses import dataclass
29
+ from datetime import timedelta
30
+ from pathlib import Path
31
+
32
+ from commitguard.exceptions.base import UnsafeInputError
33
+ from commitguard.github.errors import AppConfigurationError
34
+ from commitguard.security.secrets import Secret, register_secret
35
+ from commitguard.services.ci import DEFAULT_CI_MAX_COMMITS
36
+ from commitguard.utils.filesystem import read_bytes_limited
37
+
38
+ ENV_APP_ID = "COMMITGUARD_GITHUB_APP_ID"
39
+ ENV_PRIVATE_KEY = "COMMITGUARD_GITHUB_PRIVATE_KEY"
40
+ ENV_PRIVATE_KEY_FILE = "COMMITGUARD_GITHUB_PRIVATE_KEY_FILE"
41
+ ENV_WEBHOOK_SECRET = "COMMITGUARD_GITHUB_WEBHOOK_SECRET" # noqa: S105 - variable name
42
+ ENV_WEBHOOK_SECRET_FILE = "COMMITGUARD_GITHUB_WEBHOOK_SECRET_FILE" # noqa: S105 - variable name
43
+ ENV_DATA_DIR = "COMMITGUARD_APP_DATA_DIR"
44
+ ENV_MANDATORY_POLICY_FILE = "COMMITGUARD_APP_MANDATORY_POLICY_FILE"
45
+ ENV_WORKERS = "COMMITGUARD_APP_WORKERS"
46
+ ENV_RETENTION_DAYS = "COMMITGUARD_APP_RETENTION_DAYS"
47
+ ENV_MAX_COMMITS = "COMMITGUARD_APP_MAX_COMMITS"
48
+
49
+ MAX_PRIVATE_KEY_BYTES = 32 * 1024
50
+ MAX_WEBHOOK_SECRET_BYTES = 4096
51
+ MIN_WEBHOOK_SECRET_LENGTH = 16
52
+ DEFAULT_WORKERS = 2
53
+ DEFAULT_RETENTION_DAYS = 30
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class AppSettings:
58
+ app_id: int
59
+ private_key: Secret
60
+ webhook_secret: Secret
61
+ data_dir: Path
62
+ mandatory_policy_file: Path | None = None
63
+ workers: int = DEFAULT_WORKERS
64
+ retention: timedelta = timedelta(days=DEFAULT_RETENTION_DAYS)
65
+ max_commits: int = DEFAULT_CI_MAX_COMMITS
66
+
67
+ def __repr__(self) -> str: # secrets are already masked; keep it short anyway
68
+ return f"AppSettings(app_id={self.app_id}, data_dir={self.data_dir!s})"
69
+
70
+
71
+ def _int(environ: Mapping[str, str], name: str, default: int, *, low: int, high: int) -> int:
72
+ raw = environ.get(name)
73
+ if raw is None or raw == "":
74
+ return default
75
+ if not raw.isascii() or not raw.isdigit() or not low <= int(raw) <= high:
76
+ raise AppConfigurationError(f"{name} must be an integer between {low} and {high}")
77
+ return int(raw)
78
+
79
+
80
+ def read_app_id(environ: Mapping[str, str]) -> int:
81
+ raw = environ.get(ENV_APP_ID, "").strip()
82
+ if not raw:
83
+ raise AppConfigurationError(f"{ENV_APP_ID} is not set")
84
+ if not raw.isascii() or not raw.isdigit() or not 0 < int(raw) < 2**53:
85
+ raise AppConfigurationError(f"{ENV_APP_ID} must be a positive integer")
86
+ return int(raw)
87
+
88
+
89
+ def _read_secret(
90
+ environ: Mapping[str, str], value_var: str, file_var: str, *, max_bytes: int
91
+ ) -> tuple[str, Path | None]:
92
+ value = environ.get(value_var)
93
+ file_name = environ.get(file_var)
94
+ if value and file_name:
95
+ raise AppConfigurationError(f"set only one of {value_var} and {file_var}")
96
+ if file_name:
97
+ path = Path(file_name)
98
+ try:
99
+ data = read_bytes_limited(path, max_bytes=max_bytes)
100
+ return data.decode("utf-8"), path
101
+ except FileNotFoundError:
102
+ raise AppConfigurationError(
103
+ f"{file_var} points to a file that does not exist"
104
+ ) from None
105
+ except (OSError, UnsafeInputError, UnicodeDecodeError):
106
+ raise AppConfigurationError(f"{file_var} could not be read as a text file") from None
107
+ if value:
108
+ if len(value.encode("utf-8")) > max_bytes:
109
+ raise AppConfigurationError(f"{value_var} is too large")
110
+ return value, None
111
+ raise AppConfigurationError(f"{value_var} (or {file_var}) is not set")
112
+
113
+
114
+ def read_private_key(environ: Mapping[str, str]) -> Secret:
115
+ """The PEM text of the App private key (not yet parsed; see ``auth.AppCredentials``)."""
116
+ text, _ = _read_secret(
117
+ environ, ENV_PRIVATE_KEY, ENV_PRIVATE_KEY_FILE, max_bytes=MAX_PRIVATE_KEY_BYTES
118
+ )
119
+ if "\n" not in text and "\\n" in text:
120
+ text = text.replace("\\n", "\n") # single-line environment variable with escaped newlines
121
+ secret = Secret(text.strip() + "\n")
122
+ register_secret(secret)
123
+ return secret
124
+
125
+
126
+ def read_webhook_secret(environ: Mapping[str, str]) -> Secret:
127
+ text, from_file = _read_secret(
128
+ environ, ENV_WEBHOOK_SECRET, ENV_WEBHOOK_SECRET_FILE, max_bytes=MAX_WEBHOOK_SECRET_BYTES
129
+ )
130
+ if from_file is not None:
131
+ text = text.rstrip("\r\n")
132
+ if len(text) < MIN_WEBHOOK_SECRET_LENGTH:
133
+ raise AppConfigurationError(
134
+ f"the webhook secret must be at least {MIN_WEBHOOK_SECRET_LENGTH} characters "
135
+ "(use a long random value)"
136
+ )
137
+ secret = Secret(text)
138
+ register_secret(secret)
139
+ return secret
140
+
141
+
142
+ def private_key_file_too_open(environ: Mapping[str, str]) -> bool:
143
+ """True if the key file is readable by group or others (POSIX only)."""
144
+ name = environ.get(ENV_PRIVATE_KEY_FILE)
145
+ if not name or os.name != "posix":
146
+ return False
147
+ try:
148
+ mode = Path(name).stat().st_mode
149
+ except OSError:
150
+ return False
151
+ return bool(mode & (stat.S_IRWXG | stat.S_IRWXO))
152
+
153
+
154
+ def load_settings(environ: Mapping[str, str] | None = None) -> AppSettings:
155
+ env = os.environ if environ is None else environ
156
+ data_dir_raw = env.get(ENV_DATA_DIR, "")
157
+ if not data_dir_raw:
158
+ raise AppConfigurationError(f"{ENV_DATA_DIR} is not set")
159
+ data_dir = Path(data_dir_raw)
160
+ if not data_dir.is_absolute():
161
+ raise AppConfigurationError(f"{ENV_DATA_DIR} must be an absolute path")
162
+ policy_raw = env.get(ENV_MANDATORY_POLICY_FILE, "")
163
+ policy_file = Path(policy_raw) if policy_raw else None
164
+ retention_days = _int(env, ENV_RETENTION_DAYS, DEFAULT_RETENTION_DAYS, low=1, high=3650)
165
+ return AppSettings(
166
+ app_id=read_app_id(env),
167
+ private_key=read_private_key(env),
168
+ webhook_secret=read_webhook_secret(env),
169
+ data_dir=data_dir,
170
+ mandatory_policy_file=policy_file,
171
+ workers=_int(env, ENV_WORKERS, DEFAULT_WORKERS, low=1, high=64),
172
+ retention=timedelta(days=retention_days),
173
+ max_commits=_int(env, ENV_MAX_COMMITS, DEFAULT_CI_MAX_COMMITS, low=1, high=1_000_000),
174
+ )