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,26 @@
1
+ """Measurement and evidence: reproducible benchmarks of CommitGuard itself.
2
+
3
+ This package measures the product; it never changes a security decision. Every
4
+ benchmark drives the same code paths production uses - the
5
+ :class:`~commitguard.services.analysis.Analyzer` (detection engine + policy
6
+ evaluator), real ``git`` processes and the installed Git hooks - and records the
7
+ environment it ran in (:mod:`commitguard.research.environment`) so results can
8
+ be compared across versions and machines.
9
+
10
+ Modules::
11
+
12
+ environment benchmark manifest: versions, platform, CPU, RAM, fingerprints
13
+ datasets the labelled commit dataset (clean, violations, variations,
14
+ malformed, adversarial, generated) and its fingerprint
15
+ metrics confusion matrix, precision/recall/F1, latency percentiles
16
+ detection detection accuracy against the labelled dataset
17
+ performance detection latency, message-size scaling, CPU and memory
18
+ hooks Git operations with and without CommitGuard hooks
19
+ repository history-size scaling of range scans on real repositories
20
+ platform a portable self-check of the local enforcement path
21
+ results immutable result files and the run index
22
+ report security and benchmark reports from recorded results
23
+
24
+ Nothing here imports the GitHub App, the API or the dashboard, and nothing
25
+ contacts the network.
26
+ """
@@ -0,0 +1,231 @@
1
+ """Comparing a benchmark result with an earlier one.
2
+
3
+ Results are immutable, so comparing versions means comparing two recorded files.
4
+ This module extracts the comparable numbers from each benchmark's result document
5
+ and applies documented thresholds.
6
+
7
+ Two kinds of change are distinguished, because they deserve different responses:
8
+
9
+ * a **correctness** change - a new false negative or false positive, a platform
10
+ check that no longer passes, a seeded violation that was not found - is a
11
+ regression at any size;
12
+ * a **performance** change is a regression only beyond the threshold for that
13
+ measure, and a documented performance regression does not by itself reject a
14
+ release (see docs/maintainers/benchmarking.md).
15
+
16
+ Comparisons are only meaningful between runs of the same benchmark on comparable
17
+ machines; the environment of both runs is reported so that a reader can judge.
18
+ """
19
+
20
+ from typing import Any, Literal
21
+
22
+ from pydantic import BaseModel, ConfigDict
23
+
24
+ Verdict = Literal["improved", "unchanged", "regressed", "new", "missing"]
25
+
26
+ #: Relative change (of the worse direction) tolerated before a performance metric
27
+ #: counts as a regression. Correctness metrics use a threshold of 0.
28
+ THRESHOLDS = {
29
+ "latency": 0.20,
30
+ "throughput": 0.20,
31
+ "memory": 0.25,
32
+ "hook_overhead": 0.25,
33
+ "startup": 0.25,
34
+ "correctness": 0.0,
35
+ }
36
+
37
+
38
+ class Metric(BaseModel):
39
+ model_config = ConfigDict(frozen=True, extra="forbid")
40
+
41
+ name: str
42
+ kind: str
43
+ baseline: float | None
44
+ current: float | None
45
+ lower_is_better: bool
46
+ change_ratio: float | None
47
+ verdict: Verdict
48
+
49
+ @property
50
+ def correctness(self) -> bool:
51
+ return self.kind == "correctness"
52
+
53
+
54
+ class Comparison(BaseModel):
55
+ model_config = ConfigDict(frozen=True, extra="forbid")
56
+
57
+ benchmark: str
58
+ baseline_file: str
59
+ current_file: str
60
+ baseline_environment: str
61
+ current_environment: str
62
+ same_environment: bool
63
+ same_dataset: bool
64
+ baseline_dataset: str | None
65
+ current_dataset: str | None
66
+ metrics: tuple[Metric, ...]
67
+ regressions: tuple[str, ...]
68
+ improvements: tuple[str, ...]
69
+
70
+ @property
71
+ def ok(self) -> bool:
72
+ return not self.regressions
73
+
74
+
75
+ def _environment(document: dict[str, Any]) -> str:
76
+ manifest = document.get("manifest", {})
77
+ return (
78
+ f"{manifest.get('commitguard_version')} on {manifest.get('operating_system')} "
79
+ f"{manifest.get('os_release')} ({manifest.get('cpu_model')}), "
80
+ f"Python {manifest.get('python_version')}"
81
+ )
82
+
83
+
84
+ def _detection_metrics(result: dict[str, Any]) -> dict[str, tuple[float, str, bool]]:
85
+ decision = result.get("decision", {})
86
+ latency = result.get("latency", {})
87
+ return {
88
+ "false negatives": (decision.get("false_negative", 0), "correctness", True),
89
+ "false positives": (decision.get("false_positive", 0), "correctness", True),
90
+ "exact decisions": (result.get("exact_decision_matches", 0), "correctness", False),
91
+ "latency p50 (ms)": (latency.get("p50_ms", 0.0), "latency", True),
92
+ "latency p95 (ms)": (latency.get("p95_ms", 0.0), "latency", True),
93
+ }
94
+
95
+
96
+ def _performance_metrics(result: dict[str, Any]) -> dict[str, tuple[float, str, bool]]:
97
+ metrics: dict[str, tuple[float, str, bool]] = {
98
+ "rule loading (ms)": (result.get("startup_ms", 0.0), "startup", True)
99
+ }
100
+ for batch in result.get("batches", []):
101
+ commits = batch["commits"]
102
+ metrics[f"{commits} commits: p50 (ms)"] = (batch["latency"]["p50_ms"], "latency", True)
103
+ metrics[f"{commits} commits: commits/s"] = (
104
+ batch["commits_per_second"],
105
+ "throughput",
106
+ False,
107
+ )
108
+ for message in result.get("message_sizes", []):
109
+ size = message["message_bytes"]
110
+ metrics[f"{size} byte message: p50 (ms)"] = (message["latency"]["p50_ms"], "latency", True)
111
+ peak = result.get("peak_rss_bytes")
112
+ if peak:
113
+ metrics["peak RSS (MiB)"] = (peak / 1_048_576, "memory", True)
114
+ return metrics
115
+
116
+
117
+ def _hooks_metrics(result: dict[str, Any]) -> dict[str, tuple[float, str, bool]]:
118
+ metrics: dict[str, tuple[float, str, bool]] = {}
119
+ for overhead in result.get("overhead", []):
120
+ metrics[f"{overhead['operation']}: overhead p50 (ms)"] = (
121
+ overhead["overhead_p50_ms"],
122
+ "hook_overhead",
123
+ True,
124
+ )
125
+ for observation in result.get("observations", []):
126
+ metrics[f"observation: {observation['check']}"] = (
127
+ 1.0 if observation.get("matches") else 0.0,
128
+ "correctness",
129
+ False,
130
+ )
131
+ return metrics
132
+
133
+
134
+ def _repository_metrics(result: dict[str, Any]) -> dict[str, tuple[float, str, bool]]:
135
+ metrics: dict[str, tuple[float, str, bool]] = {}
136
+ for history in result.get("histories", []):
137
+ commits = history["commits"]
138
+ metrics[f"{commits} commits: commits/s"] = (
139
+ history["commits_per_second"],
140
+ "throughput",
141
+ False,
142
+ )
143
+ metrics[f"{commits} commits: violations found"] = (
144
+ float(history["blocked"]),
145
+ "correctness",
146
+ False,
147
+ )
148
+ return metrics
149
+
150
+
151
+ def _platform_metrics(result: dict[str, Any]) -> dict[str, tuple[float, str, bool]]:
152
+ return {
153
+ "checks passed": (float(result.get("passed", 0)), "correctness", False),
154
+ "checks failed": (float(result.get("failed", 0)), "correctness", True),
155
+ "checks skipped": (float(result.get("skipped", 0)), "correctness", True),
156
+ }
157
+
158
+
159
+ EXTRACTORS = {
160
+ "detection": _detection_metrics,
161
+ "performance": _performance_metrics,
162
+ "hooks": _hooks_metrics,
163
+ "repository": _repository_metrics,
164
+ "platform": _platform_metrics,
165
+ }
166
+
167
+
168
+ def _verdict(
169
+ baseline: float | None, current: float | None, kind: str, lower_is_better: bool
170
+ ) -> tuple[float | None, Verdict]:
171
+ if baseline is None:
172
+ return None, "new"
173
+ if current is None:
174
+ return None, "missing"
175
+ if baseline == current:
176
+ return 0.0, "unchanged"
177
+ if baseline == 0:
178
+ # No ratio is meaningful; any movement away from zero is a change.
179
+ worse = current > 0 if lower_is_better else current < 0
180
+ return None, "regressed" if worse else "improved"
181
+ ratio = (current - baseline) / abs(baseline)
182
+ worse_by = ratio if lower_is_better else -ratio
183
+ if worse_by > THRESHOLDS.get(kind, 0.0):
184
+ return ratio, "regressed"
185
+ if worse_by < 0:
186
+ return ratio, "improved"
187
+ return ratio, "unchanged"
188
+
189
+
190
+ def compare(baseline: dict[str, Any], current: dict[str, Any], *, benchmark: str) -> Comparison:
191
+ """Compare two recorded result documents of the same benchmark."""
192
+ extract = EXTRACTORS.get(benchmark)
193
+ if extract is None:
194
+ raise ValueError(f"no comparison defined for benchmark {benchmark!r}")
195
+ before, after = extract(baseline.get("result", {})), extract(current.get("result", {}))
196
+ metrics: list[Metric] = []
197
+ for name in sorted(set(before) | set(after)):
198
+ old, new = before.get(name), after.get(name)
199
+ kind = (new or old or (0.0, "correctness", True))[1]
200
+ lower_is_better = (new or old or (0.0, "correctness", True))[2]
201
+ old_value = old[0] if old else None
202
+ new_value = new[0] if new else None
203
+ ratio, verdict = _verdict(old_value, new_value, kind, lower_is_better)
204
+ metrics.append(
205
+ Metric(
206
+ name=name,
207
+ kind=kind,
208
+ baseline=old_value,
209
+ current=new_value,
210
+ lower_is_better=lower_is_better,
211
+ change_ratio=ratio,
212
+ verdict=verdict,
213
+ )
214
+ )
215
+ baseline_environment, current_environment = _environment(baseline), _environment(current)
216
+ return Comparison(
217
+ benchmark=benchmark,
218
+ baseline_file=str(baseline.get("_file", "")),
219
+ current_file=str(current.get("_file", "")),
220
+ baseline_environment=baseline_environment,
221
+ current_environment=current_environment,
222
+ same_environment=baseline.get("manifest", {}).get("cpu_model")
223
+ == current.get("manifest", {}).get("cpu_model"),
224
+ same_dataset=baseline.get("manifest", {}).get("dataset_fingerprint")
225
+ == current.get("manifest", {}).get("dataset_fingerprint"),
226
+ baseline_dataset=baseline.get("manifest", {}).get("dataset_version"),
227
+ current_dataset=current.get("manifest", {}).get("dataset_version"),
228
+ metrics=tuple(metrics),
229
+ regressions=tuple(m.name for m in metrics if m.verdict == "regressed"),
230
+ improvements=tuple(m.name for m in metrics if m.verdict == "improved"),
231
+ )