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,129 @@
|
|
|
1
|
+
"""Webhook boundary: authenticity, headers, size and JSON validation.
|
|
2
|
+
|
|
3
|
+
Processing order for every delivery (nothing untrusted is parsed before the
|
|
4
|
+
signature is verified)::
|
|
5
|
+
|
|
6
|
+
size limit -> X-Hub-Signature-256 (HMAC-SHA256, constant-time compare)
|
|
7
|
+
-> X-GitHub-Event / X-GitHub-Delivery format
|
|
8
|
+
-> strict JSON object (UTF-8, no duplicate keys, bounded depth)
|
|
9
|
+
|
|
10
|
+
A delivery that fails any step raises :class:`WebhookValidationError` carrying
|
|
11
|
+
the HTTP status to return (401 for authenticity, 400/413/415 otherwise); the
|
|
12
|
+
event is never processed. Replay protection (delivery ID deduplication) is
|
|
13
|
+
applied afterwards by :mod:`commitguard.github.storage`, because only a
|
|
14
|
+
verified delivery may consume a delivery ID.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import hmac
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
from collections.abc import Mapping
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from commitguard.github.errors import WebhookValidationError
|
|
26
|
+
from commitguard.security.secrets import Secret
|
|
27
|
+
|
|
28
|
+
SIGNATURE_HEADER = "x-hub-signature-256"
|
|
29
|
+
EVENT_HEADER = "x-github-event"
|
|
30
|
+
DELIVERY_HEADER = "x-github-delivery"
|
|
31
|
+
MAX_WEBHOOK_BYTES = 25 * 1024 * 1024 # GitHub caps webhook payloads at 25 MB
|
|
32
|
+
MAX_JSON_DEPTH = 64
|
|
33
|
+
|
|
34
|
+
_SIGNATURE_RE = re.compile(r"\Asha256=([0-9a-f]{64})\Z")
|
|
35
|
+
_EVENT_RE = re.compile(r"\A[a-z_]{1,64}\Z")
|
|
36
|
+
_DELIVERY_RE = re.compile(r"\A[0-9A-Za-z-]{8,72}\Z")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def compute_signature(secret: Secret, body: bytes) -> str:
|
|
40
|
+
"""The ``X-Hub-Signature-256`` value GitHub sends for ``body``."""
|
|
41
|
+
digest = hmac.new(secret.reveal().encode("utf-8"), body, hashlib.sha256).hexdigest()
|
|
42
|
+
return f"sha256={digest}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def verify_signature(secret: Secret, body: bytes, header: str | None) -> None:
|
|
46
|
+
"""Raise ``WebhookValidationError(401)`` unless ``header`` authenticates ``body``."""
|
|
47
|
+
if not secret:
|
|
48
|
+
raise WebhookValidationError("webhook secret is not configured", status=500)
|
|
49
|
+
if header is None or header == "":
|
|
50
|
+
raise WebhookValidationError("missing webhook signature", status=401)
|
|
51
|
+
match = _SIGNATURE_RE.match(header.strip())
|
|
52
|
+
if match is None:
|
|
53
|
+
raise WebhookValidationError("malformed webhook signature", status=401)
|
|
54
|
+
expected = compute_signature(secret, body)[len("sha256=") :]
|
|
55
|
+
if not hmac.compare_digest(expected.encode("ascii"), match.group(1).encode("ascii")):
|
|
56
|
+
raise WebhookValidationError("invalid webhook signature", status=401)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True, slots=True)
|
|
60
|
+
class WebhookDelivery:
|
|
61
|
+
delivery_id: str
|
|
62
|
+
event: str
|
|
63
|
+
payload: dict[str, Any]
|
|
64
|
+
body_sha256: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
68
|
+
result: dict[str, Any] = {}
|
|
69
|
+
for key, value in pairs:
|
|
70
|
+
if key in result:
|
|
71
|
+
raise ValueError("duplicate JSON object key")
|
|
72
|
+
result[key] = value
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _depth_ok(value: Any, limit: int) -> bool:
|
|
77
|
+
stack: list[tuple[Any, int]] = [(value, 1)]
|
|
78
|
+
while stack:
|
|
79
|
+
item, depth = stack.pop()
|
|
80
|
+
if depth > limit:
|
|
81
|
+
return False
|
|
82
|
+
if isinstance(item, dict):
|
|
83
|
+
stack.extend((v, depth + 1) for v in item.values())
|
|
84
|
+
elif isinstance(item, list):
|
|
85
|
+
stack.extend((v, depth + 1) for v in item)
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def parse_json_object(body: bytes) -> dict[str, Any]:
|
|
90
|
+
if not body:
|
|
91
|
+
raise WebhookValidationError("empty webhook payload")
|
|
92
|
+
try:
|
|
93
|
+
document = json.loads(body.decode("utf-8"), object_pairs_hook=_reject_duplicate_keys)
|
|
94
|
+
except (UnicodeDecodeError, ValueError, RecursionError):
|
|
95
|
+
raise WebhookValidationError("webhook payload is not valid JSON") from None
|
|
96
|
+
if not isinstance(document, dict):
|
|
97
|
+
raise WebhookValidationError("webhook payload must be a JSON object")
|
|
98
|
+
if not _depth_ok(document, MAX_JSON_DEPTH):
|
|
99
|
+
raise WebhookValidationError("webhook payload is nested too deeply")
|
|
100
|
+
return document
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def header(headers: Mapping[str, str], name: str) -> str | None:
|
|
104
|
+
"""Case-insensitive header lookup."""
|
|
105
|
+
lowered = name.lower()
|
|
106
|
+
for key, value in headers.items():
|
|
107
|
+
if key.lower() == lowered:
|
|
108
|
+
return value
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def parse_delivery(headers: Mapping[str, str], body: bytes, secret: Secret) -> WebhookDelivery:
|
|
113
|
+
"""Authenticate and parse one webhook delivery."""
|
|
114
|
+
if len(body) > MAX_WEBHOOK_BYTES:
|
|
115
|
+
raise WebhookValidationError("webhook payload too large", status=413)
|
|
116
|
+
verify_signature(secret, body, header(headers, SIGNATURE_HEADER))
|
|
117
|
+
event = header(headers, EVENT_HEADER)
|
|
118
|
+
if event is None or not _EVENT_RE.match(event):
|
|
119
|
+
raise WebhookValidationError("missing or invalid X-GitHub-Event header")
|
|
120
|
+
delivery_id = header(headers, DELIVERY_HEADER)
|
|
121
|
+
if delivery_id is None or not _DELIVERY_RE.match(delivery_id):
|
|
122
|
+
raise WebhookValidationError("missing or invalid X-GitHub-Delivery header")
|
|
123
|
+
payload = parse_json_object(body)
|
|
124
|
+
return WebhookDelivery(
|
|
125
|
+
delivery_id=delivery_id,
|
|
126
|
+
event=event,
|
|
127
|
+
payload=payload,
|
|
128
|
+
body_sha256=hashlib.sha256(body).hexdigest(),
|
|
129
|
+
)
|