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.
- commitguard/__init__.py +26 -0
- commitguard/__main__.py +6 -0
- commitguard/api/__init__.py +18 -0
- commitguard/api/app.py +1376 -0
- commitguard/api/governance.py +1085 -0
- commitguard/api/hosting.py +196 -0
- commitguard/api/http.py +252 -0
- commitguard/api/settings.py +169 -0
- commitguard/audit/__init__.py +13 -0
- commitguard/audit/logger.py +34 -0
- commitguard/audit/models.py +222 -0
- commitguard/audit/storage.py +59 -0
- commitguard/ci/__init__.py +7 -0
- commitguard/ci/context.py +60 -0
- commitguard/cli/__init__.py +6 -0
- commitguard/cli/app.py +74 -0
- commitguard/cli/commands/__init__.py +1 -0
- commitguard/cli/commands/benchmark.py +441 -0
- commitguard/cli/commands/check.py +100 -0
- commitguard/cli/commands/ci.py +165 -0
- commitguard/cli/commands/dashboard.py +141 -0
- commitguard/cli/commands/doctor.py +533 -0
- commitguard/cli/commands/github.py +449 -0
- commitguard/cli/commands/hook.py +156 -0
- commitguard/cli/commands/init.py +137 -0
- commitguard/cli/commands/install.py +152 -0
- commitguard/cli/commands/policy.py +36 -0
- commitguard/cli/commands/report.py +39 -0
- commitguard/cli/commands/reproduce.py +123 -0
- commitguard/cli/commands/scan.py +47 -0
- commitguard/cli/common.py +44 -0
- commitguard/cli/output.py +89 -0
- commitguard/cli/render.py +367 -0
- commitguard/config/__init__.py +6 -0
- commitguard/config/defaults.py +53 -0
- commitguard/config/enforcement.py +53 -0
- commitguard/config/loader.py +174 -0
- commitguard/config/schema.py +105 -0
- commitguard/config/sources.py +183 -0
- commitguard/controlplane/__init__.py +24 -0
- commitguard/controlplane/access.py +231 -0
- commitguard/controlplane/commands.py +393 -0
- commitguard/controlplane/errors.py +88 -0
- commitguard/controlplane/identity.py +478 -0
- commitguard/controlplane/members.py +219 -0
- commitguard/controlplane/notifications.py +787 -0
- commitguard/controlplane/pagination.py +146 -0
- commitguard/controlplane/policies.py +1204 -0
- commitguard/controlplane/queries.py +1814 -0
- commitguard/controlplane/results.py +909 -0
- commitguard/controlplane/rules.py +184 -0
- commitguard/controlplane/views.py +799 -0
- commitguard/core/__init__.py +6 -0
- commitguard/core/context.py +31 -0
- commitguard/core/decision.py +58 -0
- commitguard/core/engine.py +82 -0
- commitguard/core/result.py +177 -0
- commitguard/detectors/__init__.py +6 -0
- commitguard/detectors/base.py +58 -0
- commitguard/detectors/bot.py +87 -0
- commitguard/detectors/coauthor.py +86 -0
- commitguard/detectors/identity.py +76 -0
- commitguard/detectors/registry.py +72 -0
- commitguard/detectors/trailer.py +211 -0
- commitguard/exceptions/__init__.py +33 -0
- commitguard/exceptions/base.py +9 -0
- commitguard/exceptions/configuration.py +22 -0
- commitguard/exceptions/detection.py +11 -0
- commitguard/exceptions/git.py +41 -0
- commitguard/exceptions/service.py +25 -0
- commitguard/git/__init__.py +12 -0
- commitguard/git/commands.py +101 -0
- commitguard/git/commit.py +97 -0
- commitguard/git/diff.py +36 -0
- commitguard/git/hooks.py +527 -0
- commitguard/git/push.py +93 -0
- commitguard/git/ranges.py +71 -0
- commitguard/git/repository.py +447 -0
- commitguard/github/__init__.py +34 -0
- commitguard/github/actions.py +163 -0
- commitguard/github/app.py +935 -0
- commitguard/github/auth.py +217 -0
- commitguard/github/check_runs.py +172 -0
- commitguard/github/checks.py +210 -0
- commitguard/github/client.py +844 -0
- commitguard/github/enforcement_status.py +209 -0
- commitguard/github/errors.py +129 -0
- commitguard/github/events.py +563 -0
- commitguard/github/identifiers.py +90 -0
- commitguard/github/installations.py +566 -0
- commitguard/github/markdown.py +19 -0
- commitguard/github/permissions.py +70 -0
- commitguard/github/pull_requests.py +53 -0
- commitguard/github/queue.py +47 -0
- commitguard/github/recovery.py +124 -0
- commitguard/github/repositories.py +305 -0
- commitguard/github/server.py +52 -0
- commitguard/github/settings.py +174 -0
- commitguard/github/storage.py +2315 -0
- commitguard/github/webhooks.py +129 -0
- commitguard/github/worker.py +628 -0
- commitguard/github/workflow.py +286 -0
- commitguard/governance/__init__.py +26 -0
- commitguard/governance/bulk.py +765 -0
- commitguard/governance/cache.py +88 -0
- commitguard/governance/common.py +216 -0
- commitguard/governance/exceptions.py +861 -0
- commitguard/governance/groups.py +448 -0
- commitguard/governance/inventory.py +386 -0
- commitguard/governance/posture.py +1272 -0
- commitguard/governance/resolver.py +632 -0
- commitguard/governance/rollouts.py +760 -0
- commitguard/governance/rules.py +371 -0
- commitguard/governance/schedules.py +663 -0
- commitguard/governance/service.py +120 -0
- commitguard/governance/settings.py +365 -0
- commitguard/governance/simulation.py +618 -0
- commitguard/governance/workflow.py +734 -0
- commitguard/notifications/__init__.py +2 -0
- commitguard/notifications/channels/__init__.py +1 -0
- commitguard/notifications/channels/base.py +22 -0
- commitguard/notifications/channels/email.py +110 -0
- commitguard/notifications/channels/in_app.py +74 -0
- commitguard/notifications/channels/sink.py +58 -0
- commitguard/notifications/channels/webhook.py +233 -0
- commitguard/notifications/deduplication.py +57 -0
- commitguard/notifications/dispatcher.py +201 -0
- commitguard/notifications/models.py +439 -0
- commitguard/notifications/outbox.py +106 -0
- commitguard/notifications/preferences.py +224 -0
- commitguard/notifications/retry.py +282 -0
- commitguard/notifications/service.py +128 -0
- commitguard/notifications/settings.py +167 -0
- commitguard/notifications/templates.py +108 -0
- commitguard/observability/__init__.py +5 -0
- commitguard/observability/logging.py +161 -0
- commitguard/observability/metrics.py +105 -0
- commitguard/policies/__init__.py +6 -0
- commitguard/policies/defaults.py +48 -0
- commitguard/policies/evaluator.py +66 -0
- commitguard/policies/governance.py +498 -0
- commitguard/policies/loader.py +23 -0
- commitguard/policies/mandatory.py +52 -0
- commitguard/policies/model.py +46 -0
- commitguard/provenance/__init__.py +9 -0
- commitguard/provenance/author.py +146 -0
- commitguard/provenance/committer.py +16 -0
- commitguard/provenance/normalization.py +158 -0
- commitguard/provenance/signatures.py +34 -0
- commitguard/provenance/trailers.py +256 -0
- commitguard/research/__init__.py +26 -0
- commitguard/research/compare.py +231 -0
- commitguard/research/datasets.py +1484 -0
- commitguard/research/detection.py +183 -0
- commitguard/research/environment.py +185 -0
- commitguard/research/gitenv.py +108 -0
- commitguard/research/hooks.py +247 -0
- commitguard/research/metrics.py +85 -0
- commitguard/research/performance.py +194 -0
- commitguard/research/platform.py +288 -0
- commitguard/research/report.py +372 -0
- commitguard/research/repository.py +111 -0
- commitguard/research/reproduction.py +297 -0
- commitguard/research/results.py +94 -0
- commitguard/rules/__init__.py +11 -0
- commitguard/rules/data/ai-domains.yaml +51 -0
- commitguard/rules/data/ai-identities.yaml +131 -0
- commitguard/rules/data/bot-identities.yaml +53 -0
- commitguard/rules/data/patterns.yaml +52 -0
- commitguard/rules/loader.py +102 -0
- commitguard/rules/matcher.py +212 -0
- commitguard/rules/models.py +269 -0
- commitguard/security/__init__.py +5 -0
- commitguard/security/hashing.py +30 -0
- commitguard/security/rate_limit.py +33 -0
- commitguard/security/safe_yaml.py +69 -0
- commitguard/security/sanitization.py +85 -0
- commitguard/security/secrets.py +169 -0
- commitguard/security/validation.py +89 -0
- commitguard/services/__init__.py +15 -0
- commitguard/services/analysis.py +119 -0
- commitguard/services/audit.py +95 -0
- commitguard/services/ci.py +383 -0
- commitguard/services/enforcement.py +102 -0
- commitguard/services/hooks.py +254 -0
- commitguard/services/remediation.py +99 -0
- commitguard/services/reports.py +146 -0
- commitguard/services/scan.py +172 -0
- commitguard/utils/__init__.py +1 -0
- commitguard/utils/filesystem.py +72 -0
- commitguard/utils/platform.py +35 -0
- commitguard/utils/subprocess.py +84 -0
- commitguardian-0.1.0.dist-info/METADATA +694 -0
- commitguardian-0.1.0.dist-info/RECORD +197 -0
- commitguardian-0.1.0.dist-info/WHEEL +4 -0
- commitguardian-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|