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,72 @@
1
+ """Filesystem helpers with conservative defaults.
2
+
3
+ * Reads are size-limited and only follow regular files.
4
+ * Writes are atomic and never overwrite an existing file unless asked to.
5
+ """
6
+
7
+ import os
8
+ import stat
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+ from commitguard.exceptions.base import UnsafeInputError
13
+
14
+
15
+ def read_bytes_limited(path: Path, *, max_bytes: int) -> bytes:
16
+ """Read a regular file, refusing files larger than ``max_bytes``.
17
+
18
+ Raises :class:`FileNotFoundError` if the file is missing and
19
+ :class:`UnsafeInputError` if it is not a regular file or is too large.
20
+ """
21
+ st = path.stat() # follows symlinks deliberately; the target must be regular
22
+ if not stat.S_ISREG(st.st_mode):
23
+ raise UnsafeInputError(f"{path} is not a regular file")
24
+ if st.st_size > max_bytes:
25
+ raise UnsafeInputError(f"{path} is larger than {max_bytes} bytes")
26
+ with path.open("rb") as handle:
27
+ data = handle.read(max_bytes + 1)
28
+ if len(data) > max_bytes: # file grew between stat() and read()
29
+ raise UnsafeInputError(f"{path} is larger than {max_bytes} bytes")
30
+ return data
31
+
32
+
33
+ def read_text_limited(path: Path, *, max_bytes: int, encoding: str = "utf-8") -> str:
34
+ """Like :func:`read_bytes_limited`, decoding strictly as ``encoding``."""
35
+ data = read_bytes_limited(path, max_bytes=max_bytes)
36
+ try:
37
+ return data.decode(encoding)
38
+ except UnicodeDecodeError as exc:
39
+ raise UnsafeInputError(f"{path} is not valid {encoding} text") from exc
40
+
41
+
42
+ def atomic_write_text(
43
+ path: Path,
44
+ content: str,
45
+ *,
46
+ overwrite: bool = False,
47
+ mode: int = 0o644,
48
+ ) -> None:
49
+ """Atomically write ``content`` to ``path``.
50
+
51
+ The data is written to a temporary file in the same directory and then
52
+ moved into place. With ``overwrite=False`` (the default) an existing file
53
+ is never replaced and :class:`FileExistsError` is raised.
54
+ """
55
+ directory = path.parent
56
+ fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=directory)
57
+ tmp_path = Path(tmp_name)
58
+ try:
59
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
60
+ handle.write(content)
61
+ handle.flush()
62
+ os.fsync(handle.fileno())
63
+ tmp_path.chmod(mode)
64
+ if overwrite:
65
+ tmp_path.replace(path)
66
+ else:
67
+ # os.link fails atomically if the destination exists.
68
+ os.link(tmp_path, path)
69
+ tmp_path.unlink()
70
+ except BaseException:
71
+ tmp_path.unlink(missing_ok=True)
72
+ raise
@@ -0,0 +1,35 @@
1
+ """Platform and runtime introspection."""
2
+
3
+ import platform
4
+ import sys
5
+
6
+ MINIMUM_PYTHON = (3, 12)
7
+
8
+
9
+ def python_version() -> str:
10
+ """Return the running interpreter version as ``X.Y.Z``."""
11
+ return platform.python_version()
12
+
13
+
14
+ def python_version_supported() -> bool:
15
+ """Return True if the running interpreter meets :data:`MINIMUM_PYTHON`."""
16
+ return sys.version_info[:2] >= MINIMUM_PYTHON
17
+
18
+
19
+ def is_windows() -> bool:
20
+ """Return True on Windows (affects hook scripts and executable bits)."""
21
+ return sys.platform.startswith("win")
22
+
23
+
24
+ def path_for_posix_shell(path: str) -> str:
25
+ """Render a filesystem path for use inside a POSIX ``sh`` script.
26
+
27
+ Git for Windows runs hooks with its bundled ``sh``, which accepts
28
+ ``C:/Users/...`` style paths; backslashes would be escape characters.
29
+ """
30
+ return path.replace("\\", "/") if is_windows() else path
31
+
32
+
33
+ def supports_executable_bit() -> bool:
34
+ """False on Windows, where Git decides executability from the ``#!`` line."""
35
+ return not is_windows()
@@ -0,0 +1,84 @@
1
+ """Safe subprocess execution.
2
+
3
+ Rules enforced here, for every external command CommitGuard runs:
4
+
5
+ * commands are argument sequences, never shell strings (``shell=False``);
6
+ * arguments must be ``str`` and must not contain NUL bytes;
7
+ * stdin is closed unless input is explicitly supplied;
8
+ * every call has a timeout;
9
+ * the environment is inherited but can only be *extended* with explicit keys,
10
+ and is never logged.
11
+ """
12
+
13
+ import os
14
+ import subprocess
15
+ from collections.abc import Mapping, Sequence
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+
19
+ from commitguard.exceptions.base import UnsafeInputError
20
+
21
+ DEFAULT_TIMEOUT_SECONDS = 30.0
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class CommandResult:
26
+ """Raw result of an external command. Output is kept as bytes."""
27
+
28
+ args: tuple[str, ...]
29
+ returncode: int
30
+ stdout: bytes
31
+ stderr: bytes
32
+
33
+ @property
34
+ def ok(self) -> bool:
35
+ return self.returncode == 0
36
+
37
+
38
+ def run_command(
39
+ args: Sequence[str],
40
+ *,
41
+ cwd: Path | None = None,
42
+ env_overrides: Mapping[str, str] | None = None,
43
+ input_bytes: bytes | None = None,
44
+ timeout: float = DEFAULT_TIMEOUT_SECONDS,
45
+ ) -> CommandResult:
46
+ """Run an external command without a shell and capture its output.
47
+
48
+ Raises :class:`UnsafeInputError` for malformed argument vectors and lets
49
+ :class:`FileNotFoundError` / :class:`subprocess.TimeoutExpired` propagate so
50
+ callers can map them to domain errors.
51
+ """
52
+ if isinstance(args, str | bytes):
53
+ raise UnsafeInputError("command must be an argument sequence, not a string")
54
+ argv = tuple(args)
55
+ if not argv:
56
+ raise UnsafeInputError("command must not be empty")
57
+ for arg in argv:
58
+ if not isinstance(arg, str):
59
+ raise UnsafeInputError("command arguments must be strings")
60
+ if "\x00" in arg:
61
+ raise UnsafeInputError("command arguments must not contain NUL bytes")
62
+
63
+ env = dict(os.environ)
64
+ if env_overrides:
65
+ env.update(env_overrides)
66
+
67
+ # TODO(phase-2): cap captured output size for very large repositories.
68
+ completed = subprocess.run( # noqa: S603 - argv is validated and shell=False
69
+ argv,
70
+ cwd=cwd,
71
+ env=env,
72
+ input=input_bytes,
73
+ stdin=None if input_bytes is not None else subprocess.DEVNULL,
74
+ capture_output=True,
75
+ timeout=timeout,
76
+ shell=False,
77
+ check=False,
78
+ )
79
+ return CommandResult(
80
+ args=argv,
81
+ returncode=completed.returncode,
82
+ stdout=completed.stdout,
83
+ stderr=completed.stderr,
84
+ )