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,222 @@
|
|
|
1
|
+
"""Audit event model.
|
|
2
|
+
|
|
3
|
+
Audit events record security-relevant actions; they are not application logs.
|
|
4
|
+
Each has a stable ``type``, an **actor** (CommitGuard itself, GitHub, or a
|
|
5
|
+
signed-in dashboard user), tenant keys (``account_id`` - the GitHub
|
|
6
|
+
organisation or user account - ``installation_id``, ``repository_id``),
|
|
7
|
+
correlation IDs and a small, validated ``data`` mapping. Values are bounded
|
|
8
|
+
and sanitised on creation, and events are never updated after they are stored.
|
|
9
|
+
|
|
10
|
+
Notifications are a separate, typed stream (:mod:`commitguard.notifications`);
|
|
11
|
+
each notification records the audit correlation IDs of the change that caused it.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
import uuid
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from enum import StrEnum
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
20
|
+
|
|
21
|
+
from commitguard.core.decision import Action
|
|
22
|
+
from commitguard.security.sanitization import sanitize_for_terminal
|
|
23
|
+
from commitguard.security.secrets import redact
|
|
24
|
+
|
|
25
|
+
MAX_DATA_ITEMS = 32
|
|
26
|
+
MAX_DATA_STRING = 512
|
|
27
|
+
_KEY_RE = re.compile(r"\A[a-z][a-z0-9_]{0,63}\Z")
|
|
28
|
+
|
|
29
|
+
type AuditValue = str | int | bool | None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ActorType(StrEnum):
|
|
33
|
+
SYSTEM = "system" # CommitGuard acting on its own (scans, lifecycle updates)
|
|
34
|
+
GITHUB = "github" # a verified GitHub webhook
|
|
35
|
+
USER = "user" # a signed-in dashboard user
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Actor(BaseModel):
|
|
39
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
40
|
+
|
|
41
|
+
type: ActorType
|
|
42
|
+
id: int | None = None
|
|
43
|
+
login: str | None = Field(default=None, max_length=64)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def user(cls, user_id: int, login: str) -> "Actor":
|
|
47
|
+
return cls(type=ActorType.USER, id=user_id, login=login)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
SYSTEM_ACTOR = Actor(type=ActorType.SYSTEM)
|
|
51
|
+
GITHUB_ACTOR = Actor(type=ActorType.GITHUB)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AuditEventType(StrEnum):
|
|
55
|
+
INSTALLATION_CREATED = "installation_created"
|
|
56
|
+
INSTALLATION_REMOVED = "installation_removed"
|
|
57
|
+
INSTALLATION_SUSPENDED = "installation_suspended"
|
|
58
|
+
INSTALLATION_UNSUSPENDED = "installation_unsuspended"
|
|
59
|
+
INSTALLATION_PERMISSIONS_UPDATED = "installation_permissions_updated"
|
|
60
|
+
REPOSITORIES_ADDED = "repositories_added"
|
|
61
|
+
REPOSITORIES_REMOVED = "repositories_removed"
|
|
62
|
+
WEBHOOK_REJECTED = "webhook_rejected"
|
|
63
|
+
SCAN_QUEUED = "scan_queued"
|
|
64
|
+
REPOSITORY_SCANNED = "repository_scanned"
|
|
65
|
+
SCAN_PASSED = "scan_passed"
|
|
66
|
+
SCAN_FAILED = "scan_failed"
|
|
67
|
+
POLICY_VIOLATION = "policy_violation"
|
|
68
|
+
POLICY_MODIFICATION = "policy_modification"
|
|
69
|
+
CONFIGURATION_ERROR = "configuration_error"
|
|
70
|
+
SCAN_ERROR = "scan_error"
|
|
71
|
+
SCAN_CANCELLED = "scan_cancelled"
|
|
72
|
+
AUTHORIZATION_DENIED = "authorization_denied"
|
|
73
|
+
PULL_REQUEST_MERGED = "pull_request_merged"
|
|
74
|
+
# Control plane (dashboard) events.
|
|
75
|
+
USER_SIGNED_IN = "user_signed_in"
|
|
76
|
+
USER_SIGNED_OUT = "user_signed_out"
|
|
77
|
+
SESSION_REVOKED = "session_revoked"
|
|
78
|
+
MEMBER_ROLE_GRANTED = "member_role_granted"
|
|
79
|
+
MEMBER_ROLE_CHANGED = "member_role_changed"
|
|
80
|
+
MEMBER_REMOVED = "member_removed"
|
|
81
|
+
ORGANIZATION_POLICY_CHANGED = "organization_policy_changed"
|
|
82
|
+
VIOLATION_OPENED = "violation_opened"
|
|
83
|
+
VIOLATION_REOPENED = "violation_reopened"
|
|
84
|
+
VIOLATION_RESOLVED = "violation_resolved"
|
|
85
|
+
VIOLATION_ACKNOWLEDGED = "violation_acknowledged"
|
|
86
|
+
VIOLATION_ACKNOWLEDGEMENT_REMOVED = "violation_acknowledgement_removed"
|
|
87
|
+
REPOSITORY_MONITORING_DISABLED = "repository_monitoring_disabled"
|
|
88
|
+
REPOSITORY_MONITORING_ENABLED = "repository_monitoring_enabled"
|
|
89
|
+
REPOSITORIES_SYNCED = "repositories_synced"
|
|
90
|
+
ENFORCEMENT_STATUS_CHECKED = "enforcement_status_checked"
|
|
91
|
+
SCAN_REQUESTED = "scan_requested"
|
|
92
|
+
# Phase 7: executions, re-runs, merge queue, policy recovery, notifications.
|
|
93
|
+
SCAN_STARTED = "scan_started"
|
|
94
|
+
SCAN_RETRY_SCHEDULED = "scan_retry_scheduled"
|
|
95
|
+
CHECK_RERUN_REQUESTED = "check_rerun_requested"
|
|
96
|
+
CHECK_RERUN_REJECTED = "check_rerun_rejected"
|
|
97
|
+
MERGE_GROUP_CREATED = "merge_group_created"
|
|
98
|
+
MERGE_GROUP_PASSED = "merge_group_passed"
|
|
99
|
+
MERGE_GROUP_BLOCKED = "merge_group_blocked"
|
|
100
|
+
MERGE_GROUP_SCAN_FAILED = "merge_group_scan_failed"
|
|
101
|
+
MERGE_GROUP_DESTROYED = "merge_group_destroyed"
|
|
102
|
+
ORGANIZATION_POLICY_ROLLED_BACK = "organization_policy_rolled_back"
|
|
103
|
+
NOTIFICATION_CREATED = "notification_created"
|
|
104
|
+
NOTIFICATION_DELIVERED = "notification_delivered"
|
|
105
|
+
NOTIFICATION_DELIVERY_FAILED = "notification_delivery_failed"
|
|
106
|
+
NOTIFICATION_READ = "notification_read"
|
|
107
|
+
NOTIFICATION_SETTINGS_CHANGED = "notification_settings_changed"
|
|
108
|
+
NOTIFICATION_PREFERENCES_CHANGED = "notification_preferences_changed"
|
|
109
|
+
NOTIFICATION_WEBHOOK_ADDED = "notification_webhook_added"
|
|
110
|
+
NOTIFICATION_WEBHOOK_REMOVED = "notification_webhook_removed"
|
|
111
|
+
# Phase 8: organization governance.
|
|
112
|
+
ORGANIZATION_SETTINGS_CHANGED = "organization_settings_changed"
|
|
113
|
+
REPOSITORY_DISCOVERED = "repository_discovered"
|
|
114
|
+
REPOSITORY_ONBOARDED = "repository_onboarded"
|
|
115
|
+
REPOSITORY_EXCLUDED = "repository_excluded"
|
|
116
|
+
REPOSITORY_MODE_CHANGED = "repository_mode_changed"
|
|
117
|
+
REPOSITORY_ARCHIVED = "repository_archived"
|
|
118
|
+
REPOSITORY_SYNC_FAILED = "repository_sync_failed"
|
|
119
|
+
REPOSITORY_GROUP_CREATED = "repository_group_created"
|
|
120
|
+
REPOSITORY_GROUP_UPDATED = "repository_group_updated"
|
|
121
|
+
REPOSITORY_GROUP_ARCHIVED = "repository_group_archived"
|
|
122
|
+
REPOSITORY_GROUP_MEMBERS_ADDED = "repository_group_members_added"
|
|
123
|
+
REPOSITORY_GROUP_MEMBERS_REMOVED = "repository_group_members_removed"
|
|
124
|
+
POLICY_DRAFT_CREATED = "policy_draft_created"
|
|
125
|
+
POLICY_DRAFT_UPDATED = "policy_draft_updated"
|
|
126
|
+
POLICY_DRAFT_CANCELLED = "policy_draft_cancelled"
|
|
127
|
+
POLICY_APPROVAL_REQUESTED = "policy_approval_requested"
|
|
128
|
+
POLICY_APPROVED = "policy_approved"
|
|
129
|
+
POLICY_REJECTED = "policy_rejected"
|
|
130
|
+
POLICY_PUBLISHED = "policy_published"
|
|
131
|
+
POLICY_EMERGENCY_PUBLISHED = "policy_emergency_published"
|
|
132
|
+
POLICY_ROLLED_BACK = "policy_rolled_back"
|
|
133
|
+
POLICY_SIMULATED = "policy_simulated"
|
|
134
|
+
POLICY_ROLLOUT_STARTED = "policy_rollout_started"
|
|
135
|
+
POLICY_ROLLOUT_ADVANCED = "policy_rollout_advanced"
|
|
136
|
+
POLICY_ROLLOUT_PAUSED = "policy_rollout_paused"
|
|
137
|
+
POLICY_ROLLOUT_RESUMED = "policy_rollout_resumed"
|
|
138
|
+
POLICY_ROLLOUT_COMPLETED = "policy_rollout_completed"
|
|
139
|
+
POLICY_ROLLOUT_ROLLED_BACK = "policy_rollout_rolled_back"
|
|
140
|
+
POLICY_PROPAGATION_FAILED = "policy_propagation_failed"
|
|
141
|
+
EXCEPTION_REQUESTED = "exception_requested"
|
|
142
|
+
EXCEPTION_APPROVED = "exception_approved"
|
|
143
|
+
EXCEPTION_REJECTED = "exception_rejected"
|
|
144
|
+
EXCEPTION_CANCELLED = "exception_cancelled"
|
|
145
|
+
EXCEPTION_REVOKED = "exception_revoked"
|
|
146
|
+
EXCEPTION_EXPIRED = "exception_expired"
|
|
147
|
+
ORGANIZATION_RULES_CHANGED = "organization_rules_changed"
|
|
148
|
+
SCAN_SCHEDULE_CREATED = "scan_schedule_created"
|
|
149
|
+
SCAN_SCHEDULE_CHANGED = "scan_schedule_changed"
|
|
150
|
+
SCAN_SCHEDULE_DISABLED = "scan_schedule_disabled"
|
|
151
|
+
SCHEDULED_SCANS_QUEUED = "scheduled_scans_queued"
|
|
152
|
+
BULK_OPERATION_REQUESTED = "bulk_operation_requested"
|
|
153
|
+
BULK_OPERATION_FINISHED = "bulk_operation_finished"
|
|
154
|
+
BULK_OPERATION_CANCELLED = "bulk_operation_cancelled"
|
|
155
|
+
SECURITY_EVENT_ACKNOWLEDGED = "security_event_acknowledged"
|
|
156
|
+
REPORT_EXPORTED = "report_exported"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
#: Security-relevant events. Notifications are not generated from this list: the
|
|
160
|
+
#: domain services emit typed notification events (:mod:`commitguard.notifications`)
|
|
161
|
+
#: in the same transaction as the state change.
|
|
162
|
+
SECURITY_ALERT_TYPES = frozenset(
|
|
163
|
+
{
|
|
164
|
+
AuditEventType.POLICY_VIOLATION,
|
|
165
|
+
AuditEventType.POLICY_MODIFICATION,
|
|
166
|
+
AuditEventType.ORGANIZATION_POLICY_CHANGED,
|
|
167
|
+
AuditEventType.ORGANIZATION_POLICY_ROLLED_BACK,
|
|
168
|
+
AuditEventType.INSTALLATION_REMOVED,
|
|
169
|
+
AuditEventType.INSTALLATION_SUSPENDED,
|
|
170
|
+
AuditEventType.REPOSITORY_MONITORING_DISABLED,
|
|
171
|
+
AuditEventType.MEMBER_ROLE_GRANTED,
|
|
172
|
+
AuditEventType.MEMBER_ROLE_CHANGED,
|
|
173
|
+
AuditEventType.MERGE_GROUP_BLOCKED,
|
|
174
|
+
AuditEventType.NOTIFICATION_SETTINGS_CHANGED,
|
|
175
|
+
AuditEventType.ORGANIZATION_SETTINGS_CHANGED,
|
|
176
|
+
AuditEventType.REPOSITORY_MODE_CHANGED,
|
|
177
|
+
AuditEventType.POLICY_PUBLISHED,
|
|
178
|
+
AuditEventType.POLICY_EMERGENCY_PUBLISHED,
|
|
179
|
+
AuditEventType.POLICY_ROLLED_BACK,
|
|
180
|
+
AuditEventType.POLICY_ROLLOUT_PAUSED,
|
|
181
|
+
AuditEventType.POLICY_PROPAGATION_FAILED,
|
|
182
|
+
AuditEventType.EXCEPTION_APPROVED,
|
|
183
|
+
AuditEventType.EXCEPTION_REVOKED,
|
|
184
|
+
AuditEventType.ORGANIZATION_RULES_CHANGED,
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class AuditEvent(BaseModel):
|
|
190
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
191
|
+
|
|
192
|
+
event_id: str = Field(default_factory=lambda: uuid.uuid4().hex)
|
|
193
|
+
type: AuditEventType
|
|
194
|
+
occurred_at: datetime
|
|
195
|
+
delivery_id: str | None = None
|
|
196
|
+
request_id: str | None = None # dashboard API request
|
|
197
|
+
job_id: str | None = None
|
|
198
|
+
scan_id: str | None = None
|
|
199
|
+
actor_type: ActorType = ActorType.SYSTEM
|
|
200
|
+
actor_id: int | None = None # GitHub user ID for ActorType.USER
|
|
201
|
+
actor_login: str | None = None
|
|
202
|
+
account_id: int | None = None # filled from the installation when stored
|
|
203
|
+
installation_id: int | None = None
|
|
204
|
+
repository_id: int | None = None
|
|
205
|
+
repository: str | None = None
|
|
206
|
+
head_sha: str | None = None
|
|
207
|
+
action: Action | None = None
|
|
208
|
+
data: dict[str, AuditValue] = {}
|
|
209
|
+
|
|
210
|
+
@field_validator("data")
|
|
211
|
+
@classmethod
|
|
212
|
+
def _bounded(cls, value: dict[str, AuditValue]) -> dict[str, AuditValue]:
|
|
213
|
+
if len(value) > MAX_DATA_ITEMS:
|
|
214
|
+
raise ValueError(f"audit data has more than {MAX_DATA_ITEMS} items")
|
|
215
|
+
clean: dict[str, AuditValue] = {}
|
|
216
|
+
for key, item in value.items():
|
|
217
|
+
if not _KEY_RE.match(key):
|
|
218
|
+
raise ValueError(f"invalid audit data key {key!r}")
|
|
219
|
+
if isinstance(item, str):
|
|
220
|
+
item = sanitize_for_terminal(redact(item), max_length=MAX_DATA_STRING)
|
|
221
|
+
clean[key] = item
|
|
222
|
+
return clean
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Audit storage interface.
|
|
2
|
+
|
|
3
|
+
Storage is append-only for callers; the only deletion is retention purging.
|
|
4
|
+
Reads are always scoped to a tenant (installation, optionally repository), so a
|
|
5
|
+
future dashboard or API cannot list another organisation's events by mistake.
|
|
6
|
+
The GitHub App's SQLite state store implements this interface; a PostgreSQL
|
|
7
|
+
implementation can replace it without touching the services.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import threading
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
from commitguard.audit.models import AuditEvent
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuditStorage(ABC):
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def append_audit_event(self, event: AuditEvent) -> None:
|
|
20
|
+
"""Persist ``event``. Existing events are never updated."""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def list_audit_events(
|
|
24
|
+
self, *, installation_id: int, repository_id: int | None = None, limit: int = 100
|
|
25
|
+
) -> list[AuditEvent]:
|
|
26
|
+
"""Most recent events for one tenant, newest first."""
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def purge_audit_events(self, before: datetime) -> int:
|
|
30
|
+
"""Delete events older than ``before`` (retention). Returns the count removed."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InMemoryAuditStorage(AuditStorage):
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
self._lock = threading.Lock()
|
|
36
|
+
self._events: list[AuditEvent] = []
|
|
37
|
+
|
|
38
|
+
def append_audit_event(self, event: AuditEvent) -> None:
|
|
39
|
+
with self._lock:
|
|
40
|
+
self._events.append(event)
|
|
41
|
+
|
|
42
|
+
def list_audit_events(
|
|
43
|
+
self, *, installation_id: int, repository_id: int | None = None, limit: int = 100
|
|
44
|
+
) -> list[AuditEvent]:
|
|
45
|
+
with self._lock:
|
|
46
|
+
matching = [
|
|
47
|
+
e
|
|
48
|
+
for e in self._events
|
|
49
|
+
if e.installation_id == installation_id
|
|
50
|
+
and (repository_id is None or e.repository_id == repository_id)
|
|
51
|
+
]
|
|
52
|
+
return list(reversed(matching))[:limit]
|
|
53
|
+
|
|
54
|
+
def purge_audit_events(self, before: datetime) -> int:
|
|
55
|
+
with self._lock:
|
|
56
|
+
kept = [e for e in self._events if e.occurred_at >= before]
|
|
57
|
+
removed = len(self._events) - len(kept)
|
|
58
|
+
self._events = kept
|
|
59
|
+
return removed
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Provider-neutral CI enforcement.
|
|
2
|
+
|
|
3
|
+
CI providers (GitHub Actions today; GitLab, Bitbucket, Azure DevOps later)
|
|
4
|
+
translate their event data into a :class:`~commitguard.ci.context.CIContext`.
|
|
5
|
+
Everything after that - commit range, trusted policy source, detection, policy
|
|
6
|
+
evaluation - is shared and lives in :mod:`commitguard.services.ci`.
|
|
7
|
+
"""
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Normalised CI event context. Contains only what CommitGuard needs."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
6
|
+
|
|
7
|
+
from commitguard.security.validation import validate_git_sha
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CIProvider(StrEnum):
|
|
11
|
+
GITHUB = "github"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CIEventKind(StrEnum):
|
|
15
|
+
PULL_REQUEST = "pull_request" # changes proposed for a base branch
|
|
16
|
+
PUSH = "push" # commits that already reached the server
|
|
17
|
+
MERGE_GROUP = "merge_group" # merge queue candidate
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _oid(value: str | None) -> str | None:
|
|
21
|
+
return None if value is None else validate_git_sha(value)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CIContext(BaseModel):
|
|
25
|
+
"""What changed, according to the CI provider.
|
|
26
|
+
|
|
27
|
+
SHAs are full object IDs. For pushes, ``before_sha``/``after_sha`` are None
|
|
28
|
+
when Git reports the all-zero ID (new ref / deleted ref).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
32
|
+
|
|
33
|
+
provider: CIProvider
|
|
34
|
+
event: CIEventKind
|
|
35
|
+
event_name: str = Field(description="Provider's own event name, for display")
|
|
36
|
+
repository: str | None = None # e.g. "owner/name"
|
|
37
|
+
ref: str | None = None # fully qualified ref that changed / PR base ref
|
|
38
|
+
default_branch: str | None = None
|
|
39
|
+
base_sha: str | None = None
|
|
40
|
+
head_sha: str | None = None
|
|
41
|
+
before_sha: str | None = None
|
|
42
|
+
after_sha: str | None = None
|
|
43
|
+
pull_request_number: int | None = Field(default=None, ge=1)
|
|
44
|
+
from_fork: bool = False
|
|
45
|
+
ref_deleted: bool = False
|
|
46
|
+
|
|
47
|
+
@field_validator("base_sha", "head_sha", "before_sha", "after_sha")
|
|
48
|
+
@classmethod
|
|
49
|
+
def _valid_oids(cls, value: str | None) -> str | None:
|
|
50
|
+
return _oid(value)
|
|
51
|
+
|
|
52
|
+
@model_validator(mode="after")
|
|
53
|
+
def _consistent(self) -> "CIContext":
|
|
54
|
+
needs_both = self.event in (CIEventKind.PULL_REQUEST, CIEventKind.MERGE_GROUP)
|
|
55
|
+
if needs_both and (self.base_sha is None or self.head_sha is None):
|
|
56
|
+
raise ValueError(f"{self.event.value} requires base and head commits")
|
|
57
|
+
push_mismatch = self.ref_deleted != (self.after_sha is None)
|
|
58
|
+
if self.event is CIEventKind.PUSH and push_mismatch:
|
|
59
|
+
raise ValueError("push: after commit must be absent exactly when the ref is deleted")
|
|
60
|
+
return self
|
commitguard/cli/app.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Typer application and console entry point."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from commitguard import __version__
|
|
8
|
+
from commitguard.cli.commands import (
|
|
9
|
+
benchmark,
|
|
10
|
+
check,
|
|
11
|
+
ci,
|
|
12
|
+
dashboard,
|
|
13
|
+
doctor,
|
|
14
|
+
github,
|
|
15
|
+
hook,
|
|
16
|
+
init,
|
|
17
|
+
install,
|
|
18
|
+
policy,
|
|
19
|
+
report,
|
|
20
|
+
reproduce,
|
|
21
|
+
scan,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
app = typer.Typer(
|
|
25
|
+
name="commitguard",
|
|
26
|
+
help="Git commit provenance and contribution policy enforcement.",
|
|
27
|
+
no_args_is_help=True,
|
|
28
|
+
add_completion=False,
|
|
29
|
+
# Never render local variables in tracebacks: they may hold secrets.
|
|
30
|
+
pretty_exceptions_enable=False,
|
|
31
|
+
pretty_exceptions_show_locals=False,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _version_callback(value: bool) -> None:
|
|
36
|
+
if value:
|
|
37
|
+
typer.echo(f"commitguard {__version__}")
|
|
38
|
+
raise typer.Exit()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.callback()
|
|
42
|
+
def _root(
|
|
43
|
+
version: Annotated[
|
|
44
|
+
bool,
|
|
45
|
+
typer.Option(
|
|
46
|
+
"--version",
|
|
47
|
+
help="Show the version and exit.",
|
|
48
|
+
callback=_version_callback,
|
|
49
|
+
is_eager=True,
|
|
50
|
+
),
|
|
51
|
+
] = False,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""CommitGuard analyses commits and enforces repository contribution policies."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
app.command("init")(init.init_command)
|
|
57
|
+
app.command("install")(install.install_command)
|
|
58
|
+
app.command("uninstall")(install.uninstall_command)
|
|
59
|
+
app.command("scan")(scan.scan_command)
|
|
60
|
+
app.command("check")(check.check_command)
|
|
61
|
+
app.command("doctor")(doctor.doctor_command)
|
|
62
|
+
app.add_typer(policy.policy_app, name="policy")
|
|
63
|
+
app.add_typer(hook.hook_app, name="hook")
|
|
64
|
+
app.add_typer(ci.ci_app, name="ci")
|
|
65
|
+
app.add_typer(github.github_app, name="github")
|
|
66
|
+
app.add_typer(dashboard.dashboard_app, name="dashboard")
|
|
67
|
+
app.add_typer(benchmark.benchmark_app, name="benchmark")
|
|
68
|
+
app.add_typer(reproduce.reproduce_app, name="reproduce")
|
|
69
|
+
app.add_typer(report.report_app, name="report")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main() -> None:
|
|
73
|
+
"""Console script entry point (``commitguard``)."""
|
|
74
|
+
app()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command implementations, one module per top-level command."""
|