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,286 @@
|
|
|
1
|
+
"""GitHub workflow template and static inspection.
|
|
2
|
+
|
|
3
|
+
CommitGuard cannot see repository settings (branch protection, rulesets)
|
|
4
|
+
without the GitHub API, which Phase 4 deliberately does not use. What it can
|
|
5
|
+
do locally is inspect workflow files for the properties enforcement depends on.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict
|
|
14
|
+
|
|
15
|
+
from commitguard.exceptions.base import UnsafeInputError
|
|
16
|
+
from commitguard.security.safe_yaml import load_yaml_for_inspection
|
|
17
|
+
from commitguard.utils.filesystem import read_text_limited
|
|
18
|
+
|
|
19
|
+
WORKFLOWS_DIR = Path(".github") / "workflows"
|
|
20
|
+
WORKFLOW_FILE = WORKFLOWS_DIR / "commitguard.yml"
|
|
21
|
+
WORKFLOW_NAME = "CommitGuard"
|
|
22
|
+
JOB_ID = "commitguard"
|
|
23
|
+
#: The status check context GitHub creates for the job (the job's ``name``).
|
|
24
|
+
CHECK_NAME = "commitguard"
|
|
25
|
+
MAX_WORKFLOW_BYTES = 512 * 1024
|
|
26
|
+
|
|
27
|
+
_REPOSITORY_RE = re.compile(r"\A[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}\Z")
|
|
28
|
+
_SHA_RE = re.compile(r"\A[0-9a-f]{40}\Z")
|
|
29
|
+
_USES_PINNED_RE = re.compile(r"\A[^@\s]+@[0-9a-f]{40}\Z")
|
|
30
|
+
# "${{ ... secrets.X ... }}" in two steps: a single expression (\$\{\{[^}]*\bsecrets\.)
|
|
31
|
+
# rescans the same text from every "${{" and is quadratic on "${{${{${{..." (60 KB took
|
|
32
|
+
# 2.7 s; found by the ReDoS tests). Workflow text comes from monitored repositories.
|
|
33
|
+
_EXPRESSION_RE = re.compile(r"\$\{\{[^}]*")
|
|
34
|
+
_SECRETS_RE = re.compile(r"\bsecrets\.")
|
|
35
|
+
|
|
36
|
+
# Actions used by the template, pinned to verified commits.
|
|
37
|
+
CHECKOUT_ACTION = "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" # v4.2.2
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def render_workflow(action_repository: str, action_ref: str) -> str:
|
|
41
|
+
"""Workflow for a repository that uses the CommitGuard Action.
|
|
42
|
+
|
|
43
|
+
``action_ref`` must be a full commit SHA: the Action is part of the security
|
|
44
|
+
boundary and a movable tag or branch could be changed underneath you.
|
|
45
|
+
"""
|
|
46
|
+
if not _REPOSITORY_RE.match(action_repository):
|
|
47
|
+
raise UnsafeInputError(f"invalid action repository {action_repository!r} (owner/name)")
|
|
48
|
+
if not _SHA_RE.match(action_ref):
|
|
49
|
+
raise UnsafeInputError("action ref must be a full 40-character commit SHA")
|
|
50
|
+
return f"""\
|
|
51
|
+
# CommitGuard server-side enforcement.
|
|
52
|
+
#
|
|
53
|
+
# This check only prevents merges once branch protection (or a ruleset) marks
|
|
54
|
+
# the "{CHECK_NAME}" status check as required. See the CommitGuard documentation:
|
|
55
|
+
# docs/github-enforcement.md.
|
|
56
|
+
name: {WORKFLOW_NAME}
|
|
57
|
+
|
|
58
|
+
on:
|
|
59
|
+
pull_request:
|
|
60
|
+
merge_group:
|
|
61
|
+
push:
|
|
62
|
+
|
|
63
|
+
# The scanner only reads Git objects: no write access and no secrets.
|
|
64
|
+
permissions:
|
|
65
|
+
contents: read
|
|
66
|
+
|
|
67
|
+
# No `concurrency: cancel-in-progress`: a push check that is cancelled by a later
|
|
68
|
+
# push would leave the earlier push's commits unchecked.
|
|
69
|
+
|
|
70
|
+
jobs:
|
|
71
|
+
{JOB_ID}:
|
|
72
|
+
name: {CHECK_NAME}
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
timeout-minutes: 15
|
|
75
|
+
steps:
|
|
76
|
+
- name: Check out full history
|
|
77
|
+
uses: {CHECKOUT_ACTION} # v4.2.2
|
|
78
|
+
with:
|
|
79
|
+
fetch-depth: 0 # base and head commits must both be present
|
|
80
|
+
persist-credentials: false
|
|
81
|
+
|
|
82
|
+
- name: CommitGuard
|
|
83
|
+
uses: {action_repository}@{action_ref}
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class WorkflowIssueLevel(StrEnum):
|
|
88
|
+
OK = "ok"
|
|
89
|
+
WARN = "warn"
|
|
90
|
+
FAIL = "fail"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class WorkflowIssue(BaseModel):
|
|
94
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
95
|
+
|
|
96
|
+
level: WorkflowIssueLevel
|
|
97
|
+
message: str
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class WorkflowInspection(BaseModel):
|
|
101
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
102
|
+
|
|
103
|
+
path: Path
|
|
104
|
+
check_names: tuple[str, ...]
|
|
105
|
+
issues: tuple[WorkflowIssue, ...]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _as_list(value: Any) -> list[Any]:
|
|
109
|
+
if value is None:
|
|
110
|
+
return []
|
|
111
|
+
if isinstance(value, list):
|
|
112
|
+
return value
|
|
113
|
+
return [value]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _triggers(document: dict[Any, Any]) -> set[str]:
|
|
117
|
+
raw = document.get("on", document.get(True)) # YAML 1.1 parses a bare `on` as True
|
|
118
|
+
if isinstance(raw, str):
|
|
119
|
+
return {raw}
|
|
120
|
+
if isinstance(raw, list):
|
|
121
|
+
return {str(item) for item in raw}
|
|
122
|
+
if isinstance(raw, dict):
|
|
123
|
+
return {str(key) for key in raw}
|
|
124
|
+
return set()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _permission_issues(permissions: Any, where: str) -> list[WorkflowIssue]:
|
|
128
|
+
if permissions is None:
|
|
129
|
+
return []
|
|
130
|
+
if isinstance(permissions, str):
|
|
131
|
+
if permissions in ("read-all", "{}"):
|
|
132
|
+
return []
|
|
133
|
+
return [
|
|
134
|
+
WorkflowIssue(level=WorkflowIssueLevel.WARN, message=f"{where} grants {permissions}")
|
|
135
|
+
]
|
|
136
|
+
if isinstance(permissions, dict):
|
|
137
|
+
writes = sorted(str(k) for k, v in permissions.items() if v == "write")
|
|
138
|
+
if writes:
|
|
139
|
+
return [
|
|
140
|
+
WorkflowIssue(
|
|
141
|
+
level=WorkflowIssueLevel.WARN,
|
|
142
|
+
message=f"{where} grants write access ({', '.join(writes)}); "
|
|
143
|
+
"CommitGuard needs only contents: read",
|
|
144
|
+
)
|
|
145
|
+
]
|
|
146
|
+
return []
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _step_runs_commitguard(step: dict[Any, Any]) -> bool:
|
|
150
|
+
uses = str(step.get("uses", ""))
|
|
151
|
+
run = str(step.get("run", ""))
|
|
152
|
+
return "commitguard" in uses.lower() or "commitguard ci github" in run
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def inspect_workflow_text(path: Path, text: str) -> WorkflowInspection | None:
|
|
156
|
+
"""Inspect one workflow; None if it does not run CommitGuard."""
|
|
157
|
+
try:
|
|
158
|
+
document = load_yaml_for_inspection(text)
|
|
159
|
+
except Exception as exc: # noqa: BLE001 - any parse failure is reported, not raised
|
|
160
|
+
return WorkflowInspection(
|
|
161
|
+
path=path,
|
|
162
|
+
check_names=(),
|
|
163
|
+
issues=(
|
|
164
|
+
WorkflowIssue(
|
|
165
|
+
level=WorkflowIssueLevel.FAIL, message=f"cannot parse workflow: {exc}"
|
|
166
|
+
),
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
if not isinstance(document, dict) or not isinstance(document.get("jobs"), dict):
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
issues: list[WorkflowIssue] = []
|
|
173
|
+
check_names: list[str] = []
|
|
174
|
+
for job_id, job in document["jobs"].items():
|
|
175
|
+
if not isinstance(job, dict):
|
|
176
|
+
continue
|
|
177
|
+
steps = [s for s in _as_list(job.get("steps")) if isinstance(s, dict)]
|
|
178
|
+
if not any(_step_runs_commitguard(s) for s in steps):
|
|
179
|
+
continue
|
|
180
|
+
check_names.append(str(job.get("name", job_id)))
|
|
181
|
+
issues.extend(_permission_issues(job.get("permissions"), f"job {job_id}"))
|
|
182
|
+
checkout = [s for s in steps if str(s.get("uses", "")).startswith("actions/checkout@")]
|
|
183
|
+
if not checkout:
|
|
184
|
+
issues.append(
|
|
185
|
+
WorkflowIssue(
|
|
186
|
+
level=WorkflowIssueLevel.FAIL,
|
|
187
|
+
message=f"job {job_id} does not check out the repository",
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
elif not any(
|
|
191
|
+
isinstance(s.get("with"), dict) and str(s["with"].get("fetch-depth")) == "0"
|
|
192
|
+
for s in checkout
|
|
193
|
+
):
|
|
194
|
+
issues.append(
|
|
195
|
+
WorkflowIssue(
|
|
196
|
+
level=WorkflowIssueLevel.WARN,
|
|
197
|
+
message=f"job {job_id}: checkout without fetch-depth: 0; base commits may "
|
|
198
|
+
"be missing and the check will fail",
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
for step in steps:
|
|
202
|
+
uses = str(step.get("uses", ""))
|
|
203
|
+
if uses and not uses.startswith("./") and not _USES_PINNED_RE.match(uses):
|
|
204
|
+
issues.append(
|
|
205
|
+
WorkflowIssue(
|
|
206
|
+
level=WorkflowIssueLevel.WARN,
|
|
207
|
+
message=f"job {job_id}: {uses} is not pinned to a commit SHA",
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
if not check_names:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
triggers = _triggers(document)
|
|
214
|
+
if "pull_request_target" in triggers:
|
|
215
|
+
issues.append(
|
|
216
|
+
WorkflowIssue(
|
|
217
|
+
level=WorkflowIssueLevel.FAIL,
|
|
218
|
+
message="uses pull_request_target; use pull_request (no secrets or write token)",
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
if "pull_request" not in triggers:
|
|
222
|
+
issues.append(
|
|
223
|
+
WorkflowIssue(
|
|
224
|
+
level=WorkflowIssueLevel.WARN,
|
|
225
|
+
message="does not run on pull_request; pull requests are not checked",
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
if "merge_group" not in triggers:
|
|
229
|
+
issues.append(
|
|
230
|
+
WorkflowIssue(
|
|
231
|
+
level=WorkflowIssueLevel.OK,
|
|
232
|
+
message="no merge_group trigger (required only if you use a merge queue)",
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
concurrency = document.get("concurrency")
|
|
236
|
+
if isinstance(concurrency, dict) and concurrency.get("cancel-in-progress") is True:
|
|
237
|
+
issues.append(
|
|
238
|
+
WorkflowIssue(
|
|
239
|
+
level=WorkflowIssueLevel.WARN,
|
|
240
|
+
message="concurrency cancel-in-progress can cancel a push check whose commits "
|
|
241
|
+
"a later run does not re-check",
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
if "permissions" not in document and not all(
|
|
245
|
+
isinstance(j, dict) and "permissions" in j for j in document["jobs"].values()
|
|
246
|
+
):
|
|
247
|
+
issues.append(
|
|
248
|
+
WorkflowIssue(
|
|
249
|
+
level=WorkflowIssueLevel.WARN,
|
|
250
|
+
message="no permissions block; the default GITHUB_TOKEN may have write access",
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
issues.extend(_permission_issues(document.get("permissions"), "workflow"))
|
|
254
|
+
if any(_SECRETS_RE.search(m.group(0)) for m in _EXPRESSION_RE.finditer(text)):
|
|
255
|
+
issues.append(
|
|
256
|
+
WorkflowIssue(
|
|
257
|
+
level=WorkflowIssueLevel.WARN,
|
|
258
|
+
message="references secrets; CommitGuard scanning needs none",
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
return WorkflowInspection(path=path, check_names=tuple(check_names), issues=tuple(issues))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def inspect_repository_workflows(root: Path) -> list[WorkflowInspection]:
|
|
265
|
+
directory = root / WORKFLOWS_DIR
|
|
266
|
+
if not directory.is_dir():
|
|
267
|
+
return []
|
|
268
|
+
results = []
|
|
269
|
+
for path in sorted(directory.iterdir()):
|
|
270
|
+
if path.suffix not in (".yml", ".yaml") or not path.is_file():
|
|
271
|
+
continue
|
|
272
|
+
try:
|
|
273
|
+
text = read_text_limited(path, max_bytes=MAX_WORKFLOW_BYTES)
|
|
274
|
+
except (OSError, UnsafeInputError) as exc:
|
|
275
|
+
results.append(
|
|
276
|
+
WorkflowInspection(
|
|
277
|
+
path=path,
|
|
278
|
+
check_names=(),
|
|
279
|
+
issues=(WorkflowIssue(level=WorkflowIssueLevel.FAIL, message=str(exc)),),
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
continue
|
|
283
|
+
inspection = inspect_workflow_text(path, text)
|
|
284
|
+
if inspection is not None:
|
|
285
|
+
results.append(inspection)
|
|
286
|
+
return results
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Organization governance: central policy, groups, onboarding, exceptions and rollouts.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
Organization (GitHub account) settings.py baseline, approval, limits
|
|
6
|
+
├── repositories inventory.py discovery, onboarding, mode
|
|
7
|
+
├── repository groups groups.py
|
|
8
|
+
├── policies (organization / group / controlplane/policies.py immutable versions
|
|
9
|
+
│ repository targets) workflow.py draft -> approve -> publish
|
|
10
|
+
│ simulation.py historical, read-only
|
|
11
|
+
│ rollouts.py pilot -> stages -> active
|
|
12
|
+
├── exceptions exceptions.py scoped, expiring, approved
|
|
13
|
+
├── scan schedules schedules.py
|
|
14
|
+
├── bulk operations bulk.py background, bounded, idempotent
|
|
15
|
+
└── posture, reports, analytics posture.py
|
|
16
|
+
│
|
|
17
|
+
▼
|
|
18
|
+
GovernanceResolver (resolver.py) -> GovernanceInputs -> resolve_policy (pure)
|
|
19
|
+
│
|
|
20
|
+
▼
|
|
21
|
+
ScanService -> PolicyEvaluator -> decision -> GitHub check
|
|
22
|
+
|
|
23
|
+
The dashboard never makes a security decision: governance decides *which*
|
|
24
|
+
configuration applies, the policy engine decides what a finding means, and
|
|
25
|
+
GitHub enforces the check.
|
|
26
|
+
"""
|