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,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
|
+
)
|