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,146 @@
|
|
|
1
|
+
"""Pagination, sorting and search input for list endpoints.
|
|
2
|
+
|
|
3
|
+
* ``limit`` defaults to :data:`DEFAULT_LIMIT` and is capped at :data:`MAX_LIMIT`:
|
|
4
|
+
no endpoint returns an unbounded collection.
|
|
5
|
+
* Cursors are opaque, URL-safe strings. Large, append-mostly collections
|
|
6
|
+
(scans, violations, audit events) use keyset cursors - the sort key of the
|
|
7
|
+
last row returned - so pages stay fast and stable while new rows arrive.
|
|
8
|
+
Small collections (repositories, members, policy versions) use offset cursors.
|
|
9
|
+
A cursor only encodes a position: it grants nothing, because every page is
|
|
10
|
+
still filtered by the caller's access scope.
|
|
11
|
+
* Sort keys come from per-endpoint allow-lists; a query parameter never names a
|
|
12
|
+
column.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import base64
|
|
16
|
+
import binascii
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from datetime import UTC, datetime
|
|
22
|
+
|
|
23
|
+
from commitguard.controlplane.errors import InputValidationError
|
|
24
|
+
|
|
25
|
+
DEFAULT_LIMIT = 25
|
|
26
|
+
MAX_LIMIT = 100
|
|
27
|
+
MAX_CURSOR_CHARS = 512
|
|
28
|
+
MAX_SEARCH_CHARS = 100
|
|
29
|
+
MAX_OFFSET = 100_000
|
|
30
|
+
|
|
31
|
+
type CursorValue = str | int | float | None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class Page[T]:
|
|
36
|
+
items: list[T]
|
|
37
|
+
next_cursor: str | None
|
|
38
|
+
limit: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_limit(raw: str | None) -> int:
|
|
42
|
+
if raw is None or raw == "":
|
|
43
|
+
return DEFAULT_LIMIT
|
|
44
|
+
if not raw.isascii() or not raw.isdigit() or len(raw) > 4:
|
|
45
|
+
raise InputValidationError("limit must be a positive integer", field="limit")
|
|
46
|
+
value = int(raw)
|
|
47
|
+
if not 1 <= value <= MAX_LIMIT:
|
|
48
|
+
raise InputValidationError(f"limit must be between 1 and {MAX_LIMIT}", field="limit")
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def encode_cursor(values: Sequence[CursorValue]) -> str:
|
|
53
|
+
raw = json.dumps(list(values), separators=(",", ":")).encode("utf-8")
|
|
54
|
+
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def decode_cursor(cursor: str | None, kinds: Sequence[type]) -> list[CursorValue] | None:
|
|
58
|
+
"""Decode a cursor whose values must have exactly the given types."""
|
|
59
|
+
if cursor is None or cursor == "":
|
|
60
|
+
return None
|
|
61
|
+
if len(cursor) > MAX_CURSOR_CHARS or not re.fullmatch(r"[A-Za-z0-9_-]+", cursor):
|
|
62
|
+
raise InputValidationError("invalid cursor", field="cursor")
|
|
63
|
+
try:
|
|
64
|
+
padded = cursor + "=" * (-len(cursor) % 4)
|
|
65
|
+
values = json.loads(base64.urlsafe_b64decode(padded).decode("utf-8"))
|
|
66
|
+
except (binascii.Error, ValueError, UnicodeDecodeError):
|
|
67
|
+
raise InputValidationError("invalid cursor", field="cursor") from None
|
|
68
|
+
if not isinstance(values, list) or len(values) != len(kinds):
|
|
69
|
+
raise InputValidationError("invalid cursor", field="cursor")
|
|
70
|
+
for value, kind in zip(values, kinds, strict=True):
|
|
71
|
+
if kind is float and isinstance(value, int) and not isinstance(value, bool):
|
|
72
|
+
continue
|
|
73
|
+
if not isinstance(value, kind) or isinstance(value, bool):
|
|
74
|
+
raise InputValidationError("invalid cursor", field="cursor")
|
|
75
|
+
return values
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def offset_cursor(cursor: str | None) -> int:
|
|
79
|
+
values = decode_cursor(cursor, (int,))
|
|
80
|
+
offset = int(values[0]) if values else 0 # type: ignore[arg-type]
|
|
81
|
+
if not 0 <= offset <= MAX_OFFSET:
|
|
82
|
+
raise InputValidationError("invalid cursor", field="cursor")
|
|
83
|
+
return offset
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parse_search(raw: str | None) -> str | None:
|
|
87
|
+
"""Normalised free-text search: trimmed, bounded, printable."""
|
|
88
|
+
if raw is None:
|
|
89
|
+
return None
|
|
90
|
+
value = " ".join(raw.split())
|
|
91
|
+
if not value:
|
|
92
|
+
return None
|
|
93
|
+
if len(value) > MAX_SEARCH_CHARS:
|
|
94
|
+
raise InputValidationError(
|
|
95
|
+
f"search must be at most {MAX_SEARCH_CHARS} characters", field="q"
|
|
96
|
+
)
|
|
97
|
+
if any(ord(c) < 0x20 or 0x7F <= ord(c) <= 0x9F for c in value):
|
|
98
|
+
raise InputValidationError("search contains control characters", field="q")
|
|
99
|
+
return value
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def like_pattern(value: str) -> str:
|
|
103
|
+
"""A LIKE pattern matching ``value`` literally anywhere (``ESCAPE '\\'``)."""
|
|
104
|
+
escaped = value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
|
105
|
+
return f"%{escaped}%"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def parse_choice[E](raw: str | None, choices: dict[str, E], field: str) -> E | None:
|
|
109
|
+
if raw is None or raw == "":
|
|
110
|
+
return None
|
|
111
|
+
if raw not in choices:
|
|
112
|
+
allowed = ", ".join(sorted(choices))
|
|
113
|
+
raise InputValidationError(f"{field} must be one of: {allowed}", field=field)
|
|
114
|
+
return choices[raw]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def parse_int_id(raw: str | None, field: str) -> int | None:
|
|
118
|
+
if raw is None or raw == "":
|
|
119
|
+
return None
|
|
120
|
+
if not raw.isascii() or not raw.isdigit() or len(raw) > 16 or int(raw) <= 0:
|
|
121
|
+
raise InputValidationError(f"{field} must be a positive integer", field=field)
|
|
122
|
+
return int(raw)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def parse_timestamp(raw: str | None, field: str) -> datetime | None:
|
|
126
|
+
"""An ISO 8601 date or date-time; naive values are UTC."""
|
|
127
|
+
if raw is None or raw == "":
|
|
128
|
+
return None
|
|
129
|
+
if len(raw) > 40:
|
|
130
|
+
raise InputValidationError(f"{field} must be an ISO 8601 date", field=field)
|
|
131
|
+
try:
|
|
132
|
+
value = datetime.fromisoformat(raw)
|
|
133
|
+
except ValueError:
|
|
134
|
+
raise InputValidationError(f"{field} must be an ISO 8601 date", field=field) from None
|
|
135
|
+
return value if value.tzinfo else value.replace(tzinfo=UTC)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
_HEX_RE = re.compile(r"\A[0-9a-f]{4,64}\Z")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def sha_prefix(value: str | None) -> str | None:
|
|
142
|
+
"""A lower-case hex prefix usable for commit SHA search, or None."""
|
|
143
|
+
if value is None:
|
|
144
|
+
return None
|
|
145
|
+
candidate = value.lower()
|
|
146
|
+
return candidate if _HEX_RE.match(candidate) else None
|