claude-dev-env 8.6.0 → 8.7.0
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.
- package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +1 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +3 -2
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +35 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +4 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +155 -18
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +130 -0
- package/bin/install.test.mjs +25 -0
- package/docs/high-trust-agent-delivery.md +132 -0
- package/hooks/advisory/refactor_guard.py +2 -2
- package/hooks/advisory/test_refactor_guard.py +21 -0
- package/hooks/blocking/code_rules_enforcer_test_support.py +10 -0
- package/hooks/blocking/code_rules_shared.py +4 -7
- package/hooks/blocking/conftest.py +22 -0
- package/hooks/blocking/state_description_blocker.py +232 -90
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +2 -11
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +27 -54
- package/hooks/blocking/test_code_rules_enforcer_test_support.py +21 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +3 -19
- package/hooks/blocking/test_code_rules_shared.py +18 -0
- package/hooks/blocking/test_conftest.py +35 -0
- package/hooks/blocking/test_state_description_blocker.py +59 -0
- package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -14
- package/hooks/git-hooks/pull_request_handoff.py +2 -2
- package/hooks/git-hooks/test_config.py +10 -3
- package/hooks/git-hooks/test_pull_request_handoff.py +49 -0
- package/hooks/git-hooks/test_verification_notice_state.py +50 -0
- package/hooks/git-hooks/verification_notice_state.py +3 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +13 -5
- package/hooks/hooks_constants/refactor_guard_constants.py +2 -0
- package/hooks/hooks_constants/session_edit_stage_gate_constants.py +3 -4
- package/hooks/hooks_constants/state_description_blocker_constants.py +2 -1
- package/hooks/hooks_constants/test_state_description_blocker_constants.py +87 -0
- package/hooks/validators/exempt_paths.py +3 -18
- package/hooks/validators/test_exempt_paths.py +20 -0
- package/hooks/validators/test_python_antipattern_checks.py +14 -0
- package/package.json +1 -1
- package/rules/agent-merges-its-own-green-pull-request.md +54 -0
- package/rules/archiving-agent-config.md +10 -0
- package/rules/code-standards.md +1 -0
- package/rules/correction-lens.md +134 -0
- package/rules/flag-non-breaking-findings.md +11 -0
- package/rules/no-contrast-framing.md +68 -0
- package/rules/review-closure-is-a-check.md +44 -0
- package/scripts/Invoke-PesterSuites.ps1 +42 -0
- package/scripts/agent_merge_check.py +381 -0
- package/scripts/check.ps1 +12 -1
- package/scripts/ci/install-playtest.mjs +291 -0
- package/scripts/ci/install-playtest.test.mjs +84 -0
- package/scripts/ci/installer-lifecycle.mjs +11 -30
- package/scripts/ci/installer-lifecycle.test.mjs +14 -0
- package/scripts/ci/pstack-command-shim.test.mjs +33 -0
- package/scripts/ci/scratch-home-install.mjs +73 -0
- package/scripts/ci/scratch-home-install.test.mjs +49 -0
- package/scripts/codex_compat_materializer.py +33 -16
- package/scripts/contrast_framing.py +101 -0
- package/scripts/dev_env_scripts_constants/agent_merge_check_constants.py +121 -0
- package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +82 -0
- package/scripts/dev_env_scripts_constants/merge_gate_check_constants.py +23 -0
- package/scripts/dev_env_scripts_constants/review_closure_constants.py +241 -0
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +12 -0
- package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +60 -0
- package/scripts/durable_post_lint.py +30 -4
- package/scripts/followup_cli.py +6 -10
- package/scripts/merge_gate_checks.py +236 -0
- package/scripts/policy_lint/adapter_contrast_framing.py +59 -0
- package/scripts/policy_lint/adapter_detectors.py +1 -1
- package/scripts/policy_lint/adapter_support.py +3 -14
- package/scripts/policy_lint/adapters.py +30 -1
- package/scripts/policy_lint/config/constants.py +2 -0
- package/scripts/policy_lint/registry.py +5 -0
- package/scripts/repository_checks/config/constants.py +12 -0
- package/scripts/repository_checks/config/policy_document.py +28 -2
- package/scripts/repository_checks/followups.py +54 -0
- package/scripts/repository_checks/match_exemptions.py +67 -0
- package/scripts/repository_checks/models.py +46 -2
- package/scripts/repository_checks/reporting.py +23 -7
- package/scripts/repository_checks/tracked_secrets.py +2 -2
- package/scripts/repository_policy.py +4 -2
- package/scripts/review_closure.py +134 -0
- package/scripts/review_closure_github.py +345 -0
- package/scripts/review_closure_model.py +360 -0
- package/scripts/test_agent_merge_check.py +317 -0
- package/scripts/test_durable_post_lint.py +46 -0
- package/scripts/test_followup_cli.py +40 -0
- package/scripts/test_merge_gate_checks.py +92 -0
- package/scripts/tests/test_code_review_constants.py +78 -0
- package/scripts/tests/test_codex_compat_materializer.py +48 -4
- package/scripts/tests/test_contrast_framing.py +117 -0
- package/scripts/tests/test_followups.py +88 -0
- package/scripts/tests/test_match_exemptions.py +291 -0
- package/scripts/tests/test_models.py +67 -2
- package/scripts/tests/test_package_inventory.py +6 -4
- package/scripts/tests/test_pester_suites_run.py +53 -0
- package/scripts/tests/test_policy_document.py +39 -1
- package/scripts/tests/test_policy_lint_rules_contrast_framing.py +68 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +4 -1
- package/scripts/tests/test_reporting.py +18 -0
- package/scripts/tests/test_repository_policy.py +43 -1
- package/scripts/tests/test_review_closure.py +126 -0
- package/scripts/tests/test_review_closure_github.py +240 -0
- package/scripts/tests/test_review_closure_model.py +299 -0
- package/scripts/tests/test_tracked_secrets.py +42 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +0 -12
- package/scripts/repository_checks/email_exemptions.py +0 -39
- package/scripts/tests/test_email_exemptions.py +0 -158
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
|
|
7
|
+
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
8
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
9
|
+
|
|
10
|
+
import review_closure_model as model
|
|
11
|
+
from dev_env_scripts_constants.review_closure_constants import (
|
|
12
|
+
APPROVALS_CHECK_NAME,
|
|
13
|
+
APPROVALS_OPEN_REASON,
|
|
14
|
+
RED_CIRCLE_MARKER,
|
|
15
|
+
RED_CIRCLE_OPEN_REASON,
|
|
16
|
+
UNANSWERED_OPEN_REASON,
|
|
17
|
+
UNNAMED_THREAD_SUBJECT,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
DRIVING_AGENT = frozenset({"claude[bot]"})
|
|
21
|
+
REVIEW_BOT = "qodo-merge-pro[bot]"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def thread(
|
|
25
|
+
*,
|
|
26
|
+
all_comments: tuple[model.ReviewComment, ...],
|
|
27
|
+
is_resolved: bool = False,
|
|
28
|
+
is_outdated: bool = False,
|
|
29
|
+
subject: str = "scripts/example.py",
|
|
30
|
+
) -> model.ReviewThread:
|
|
31
|
+
return model.ReviewThread(
|
|
32
|
+
subject=subject,
|
|
33
|
+
is_resolved=is_resolved,
|
|
34
|
+
is_outdated=is_outdated,
|
|
35
|
+
all_comments=all_comments,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def bot_comment(body: str = "This call drops the return value.") -> model.ReviewComment:
|
|
40
|
+
return model.ReviewComment(author_login=REVIEW_BOT, body=body)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def agent_comment(body: str = "Fixed in the next push.") -> model.ReviewComment:
|
|
44
|
+
return model.ReviewComment(author_login="claude[bot]", body=body)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def should_report_an_unanswered_thread_as_open() -> None:
|
|
48
|
+
finding = model.thread_finding(thread(all_comments=(bot_comment(),)), DRIVING_AGENT)
|
|
49
|
+
|
|
50
|
+
assert finding == model.OpenFinding(
|
|
51
|
+
subject="scripts/example.py", reason=UNANSWERED_OPEN_REASON
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def should_close_a_thread_the_driving_agent_replied_to() -> None:
|
|
56
|
+
answered = thread(all_comments=(bot_comment(), agent_comment()))
|
|
57
|
+
|
|
58
|
+
assert model.thread_finding(answered, DRIVING_AGENT) is None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def should_leave_a_thread_open_when_only_another_bot_replied() -> None:
|
|
62
|
+
other_bot = model.ReviewComment(author_login="graphite[bot]", body="Same here.")
|
|
63
|
+
noisy = thread(all_comments=(bot_comment(), other_bot))
|
|
64
|
+
|
|
65
|
+
assert model.thread_finding(noisy, DRIVING_AGENT) is not None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def should_close_an_outdated_thread_without_a_reply() -> None:
|
|
69
|
+
replaced = thread(all_comments=(bot_comment(),), is_outdated=True)
|
|
70
|
+
|
|
71
|
+
assert model.thread_finding(replaced, DRIVING_AGENT) is None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def should_close_a_resolved_ordinary_thread() -> None:
|
|
75
|
+
resolved = thread(all_comments=(bot_comment(),), is_resolved=True)
|
|
76
|
+
|
|
77
|
+
assert model.thread_finding(resolved, DRIVING_AGENT) is None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def should_keep_a_resolved_red_circle_finding_open() -> None:
|
|
81
|
+
blocking = thread(
|
|
82
|
+
all_comments=(bot_comment(f"{RED_CIRCLE_MARKER} This drops user input."),),
|
|
83
|
+
is_resolved=True,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
finding = model.thread_finding(blocking, DRIVING_AGENT)
|
|
87
|
+
|
|
88
|
+
assert finding == model.OpenFinding(
|
|
89
|
+
subject="scripts/example.py", reason=RED_CIRCLE_OPEN_REASON
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def should_close_a_red_circle_finding_the_agent_answered() -> None:
|
|
94
|
+
answered = thread(
|
|
95
|
+
all_comments=(
|
|
96
|
+
bot_comment(f"{RED_CIRCLE_MARKER} This drops user input."),
|
|
97
|
+
agent_comment(),
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
assert model.thread_finding(answered, DRIVING_AGENT) is None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def should_close_a_red_circle_finding_a_push_replaced() -> None:
|
|
105
|
+
replaced = thread(
|
|
106
|
+
all_comments=(bot_comment(f"{RED_CIRCLE_MARKER} This drops user input."),),
|
|
107
|
+
is_outdated=True,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
assert model.thread_finding(replaced, DRIVING_AGENT) is None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def should_close_a_thread_the_driving_agent_opened() -> None:
|
|
114
|
+
own = thread(all_comments=(agent_comment("Reading this back for the reviewer."),))
|
|
115
|
+
|
|
116
|
+
assert model.thread_finding(own, DRIVING_AGENT) is None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def should_report_the_driving_agent_as_the_thread_author() -> None:
|
|
120
|
+
own = thread(all_comments=(agent_comment(),))
|
|
121
|
+
|
|
122
|
+
assert model.opened_by_driver(own, DRIVING_AGENT) is True
|
|
123
|
+
assert model.opened_by_driver(thread(all_comments=()), DRIVING_AGENT) is False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def should_report_a_reply_from_the_driving_agent() -> None:
|
|
127
|
+
answered = thread(all_comments=(bot_comment(), agent_comment()))
|
|
128
|
+
|
|
129
|
+
assert model.has_driver_reply(answered, DRIVING_AGENT) is True
|
|
130
|
+
assert (
|
|
131
|
+
model.has_driver_reply(thread(all_comments=(bot_comment(),)), DRIVING_AGENT)
|
|
132
|
+
is False
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def should_report_a_blocking_approvals_row_as_open() -> None:
|
|
137
|
+
all_findings = model.all_open_findings((), DRIVING_AGENT, "failure")
|
|
138
|
+
|
|
139
|
+
assert all_findings == (
|
|
140
|
+
model.OpenFinding(subject=APPROVALS_CHECK_NAME, reason=APPROVALS_OPEN_REASON),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def should_pass_when_approvals_succeeds_and_no_thread_waits() -> None:
|
|
145
|
+
answered = thread(all_comments=(bot_comment(), agent_comment()))
|
|
146
|
+
|
|
147
|
+
assert model.all_open_findings((answered,), DRIVING_AGENT, "success") == ()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def should_pass_where_the_approvals_check_does_not_run() -> None:
|
|
151
|
+
assert model.all_open_findings((), DRIVING_AGENT, None) == ()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def should_report_every_open_thread_and_the_approvals_row() -> None:
|
|
155
|
+
first = thread(all_comments=(bot_comment(),), subject="scripts/first.py")
|
|
156
|
+
second = thread(all_comments=(bot_comment(),), subject="scripts/second.py")
|
|
157
|
+
|
|
158
|
+
all_findings = model.all_open_findings(
|
|
159
|
+
(first, second), DRIVING_AGENT, "action_required"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
assert [each.subject for each in all_findings] == [
|
|
163
|
+
"scripts/first.py",
|
|
164
|
+
"scripts/second.py",
|
|
165
|
+
APPROVALS_CHECK_NAME,
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def should_read_a_graphql_thread_record() -> None:
|
|
170
|
+
parsed = model.parse_thread(
|
|
171
|
+
{
|
|
172
|
+
"path": "scripts/example.py",
|
|
173
|
+
"isResolved": False,
|
|
174
|
+
"isOutdated": True,
|
|
175
|
+
"comments": {
|
|
176
|
+
"nodes": [
|
|
177
|
+
{"body": "A finding.", "author": {"login": REVIEW_BOT}},
|
|
178
|
+
{"body": "Answered.", "author": {"login": "claude[bot]"}},
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
assert parsed == model.ReviewThread(
|
|
185
|
+
subject="scripts/example.py",
|
|
186
|
+
is_resolved=False,
|
|
187
|
+
is_outdated=True,
|
|
188
|
+
all_comments=(
|
|
189
|
+
model.ReviewComment(author_login=REVIEW_BOT, body="A finding."),
|
|
190
|
+
model.ReviewComment(author_login="claude[bot]", body="Answered."),
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def should_read_a_rest_thread_record() -> None:
|
|
196
|
+
parsed = model.parse_thread(
|
|
197
|
+
{
|
|
198
|
+
"resolved": True,
|
|
199
|
+
"outdated": False,
|
|
200
|
+
"comments": [{"body": "A finding.", "user": {"login": REVIEW_BOT}}],
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
assert parsed == model.ReviewThread(
|
|
205
|
+
subject=UNNAMED_THREAD_SUBJECT,
|
|
206
|
+
is_resolved=True,
|
|
207
|
+
is_outdated=False,
|
|
208
|
+
all_comments=(model.ReviewComment(author_login=REVIEW_BOT, body="A finding."),),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def should_read_a_thread_that_carries_no_comments() -> None:
|
|
213
|
+
parsed = model.parse_thread({"path": "scripts/example.py"})
|
|
214
|
+
|
|
215
|
+
assert parsed.all_comments == ()
|
|
216
|
+
assert model.carries_red_circle(parsed) is False
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def should_find_the_approvals_conclusion_among_the_check_runs() -> None:
|
|
220
|
+
all_check_runs = [
|
|
221
|
+
{"name": "Python suite (ubuntu)", "conclusion": "success"},
|
|
222
|
+
{"name": APPROVALS_CHECK_NAME, "conclusion": "failure"},
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
assert model.approvals_conclusion(all_check_runs) == "failure"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def should_report_no_conclusion_where_approvals_does_not_run() -> None:
|
|
229
|
+
assert model.approvals_conclusion([{"name": "Python suite (ubuntu)"}]) is None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def should_count_the_pull_request_author_as_the_driving_agent() -> None:
|
|
233
|
+
all_logins = model.driver_logins({"user": {"login": "claude[bot]"}}, [])
|
|
234
|
+
|
|
235
|
+
assert all_logins == frozenset({"claude[bot]"})
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def should_add_each_login_named_on_the_command_line() -> None:
|
|
239
|
+
all_logins = model.driver_logins({"user": {"login": "claude[bot]"}}, ["JonEcho"])
|
|
240
|
+
|
|
241
|
+
assert all_logins == frozenset({"claude[bot]", "JonEcho"})
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def should_read_the_head_commit_from_the_pull_request() -> None:
|
|
245
|
+
assert model.head_sha({"head": {"sha": "ab845eb"}}) == "ab845eb"
|
|
246
|
+
assert model.head_sha({}) == ""
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def should_name_the_open_count_in_the_verdict_line() -> None:
|
|
250
|
+
line = model.verdict_line(
|
|
251
|
+
"jl-cmd/claude-dev-env",
|
|
252
|
+
{"number": 1442, "head": {"sha": "ab845ebc0ffee11"}},
|
|
253
|
+
(model.OpenFinding(subject="scripts/example.py", reason="waiting"),),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
assert line.startswith("OPEN jl-cmd/claude-dev-env#1442 ab845eb :: 1 review")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def should_name_the_closed_state_in_the_verdict_line() -> None:
|
|
260
|
+
line = model.verdict_line(
|
|
261
|
+
"jl-cmd/claude-dev-env",
|
|
262
|
+
{"number": 1442, "head": {"sha": "ab845ebc0ffee11"}},
|
|
263
|
+
(),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
assert line.startswith("CLOSED jl-cmd/claude-dev-env#1442 ab845eb ::")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def should_join_a_session_route_thread_with_its_comments() -> None:
|
|
270
|
+
parsed = model.parse_thread(
|
|
271
|
+
{"path": "scripts/example.py", "resolved": False, "comment_ids": [11, 12]},
|
|
272
|
+
{
|
|
273
|
+
11: {"body": "A finding.", "user": {"login": REVIEW_BOT}},
|
|
274
|
+
12: {"body": "Answered.", "user": {"login": "claude[bot]"}},
|
|
275
|
+
},
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
assert [each.author_login for each in parsed.all_comments] == [
|
|
279
|
+
REVIEW_BOT,
|
|
280
|
+
"claude[bot]",
|
|
281
|
+
]
|
|
282
|
+
assert model.thread_finding(parsed, DRIVING_AGENT) is None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def should_skip_a_comment_identifier_the_listing_does_not_carry() -> None:
|
|
286
|
+
parsed = model.parse_thread(
|
|
287
|
+
{"path": "scripts/example.py", "comment_ids": [11, 99]},
|
|
288
|
+
{11: {"body": "A finding.", "user": {"login": REVIEW_BOT}}},
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
assert len(parsed.all_comments) == 1
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def should_key_the_review_comments_by_identifier() -> None:
|
|
295
|
+
all_comment_records = model.comment_records_by_id(
|
|
296
|
+
[{"id": 11, "body": "A finding."}, {"body": "no identifier"}, "not a record"]
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
assert all_comment_records == {11: {"id": 11, "body": "A finding."}}
|
|
@@ -45,6 +45,9 @@ _PII_PREVENTION_CONSTANTS = load_hooks_module(
|
|
|
45
45
|
)
|
|
46
46
|
_PII_CATEGORY_EMAIL = _PII_PREVENTION_CONSTANTS.CATEGORY_EMAIL
|
|
47
47
|
_PII_CATEGORY_HOME_PATH = _PII_PREVENTION_CONSTANTS.CATEGORY_HOME_PATH
|
|
48
|
+
_PII_CATEGORY_PRIVATE_IP = _PII_PREVENTION_CONSTANTS.CATEGORY_PRIVATE_IP
|
|
49
|
+
_OWNED_ADDRESS = "10.83.21.14"
|
|
50
|
+
_OWNED_ADDRESS_RELATIVE_PATH = "src/runner.py"
|
|
48
51
|
|
|
49
52
|
|
|
50
53
|
def test_should_flag_a_tracked_secret(tmp_path: Path) -> None:
|
|
@@ -265,3 +268,42 @@ def _install_exact_email_exemption(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
265
268
|
}
|
|
266
269
|
),
|
|
267
270
|
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_should_clear_a_private_address_a_committed_entry_owns(tmp_path: Path) -> None:
|
|
274
|
+
repository_root = tmp_path / "repo"
|
|
275
|
+
initialize_repository(repository_root)
|
|
276
|
+
write_text(
|
|
277
|
+
repository_root / _OWNED_ADDRESS_RELATIVE_PATH, f"host = '{_OWNED_ADDRESS}'\n"
|
|
278
|
+
)
|
|
279
|
+
write_text(
|
|
280
|
+
repository_root / _OTHER_NOTES_RELATIVE_PATH, f"host = '{_OWNED_ADDRESS}'\n"
|
|
281
|
+
)
|
|
282
|
+
write_text(
|
|
283
|
+
repository_root / "config" / "repository-policy.json",
|
|
284
|
+
json.dumps(
|
|
285
|
+
{
|
|
286
|
+
"version": 1,
|
|
287
|
+
"private_ip_exemptions": [
|
|
288
|
+
{
|
|
289
|
+
"path": _OWNED_ADDRESS_RELATIVE_PATH,
|
|
290
|
+
"sha256": hashlib.sha256(
|
|
291
|
+
_OWNED_ADDRESS.encode(_UTF8_ENCODING)
|
|
292
|
+
).hexdigest(),
|
|
293
|
+
"reason": "Documented address of the shared build runner",
|
|
294
|
+
}
|
|
295
|
+
],
|
|
296
|
+
}
|
|
297
|
+
),
|
|
298
|
+
)
|
|
299
|
+
commit_tracked_files(repository_root)
|
|
300
|
+
|
|
301
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
302
|
+
|
|
303
|
+
assert _PII_CATEGORY_PRIVATE_IP not in _pii_categories_for_path(
|
|
304
|
+
stdout_text, _OWNED_ADDRESS_RELATIVE_PATH
|
|
305
|
+
)
|
|
306
|
+
assert _PII_CATEGORY_PRIVATE_IP in _pii_categories_for_path(
|
|
307
|
+
stdout_text, _OTHER_NOTES_RELATIVE_PATH
|
|
308
|
+
)
|
|
309
|
+
assert exit_code == FINDINGS_EXIT_CODE
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
"""Constants for git-repository-root resolution.
|
|
2
|
-
|
|
3
|
-
The git-repository-root resolution command and its timeout, used by
|
|
4
|
-
``resolve_repository_root`` to find the repository a directory belongs to.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
GIT_COMMAND_TIMEOUT_SECONDS: int = 5
|
|
8
|
-
ALL_GIT_REPOSITORY_ROOT_COMMAND: tuple[str, ...] = (
|
|
9
|
-
"git",
|
|
10
|
-
"rev-parse",
|
|
11
|
-
"--show-toplevel",
|
|
12
|
-
)
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"""Load exact public-email exceptions owned by a repository."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
from repository_checks.config.policy_document import (
|
|
8
|
-
EMAIL_CATEGORY,
|
|
9
|
-
EMAIL_EXEMPTION_SUBJECT,
|
|
10
|
-
EMAIL_EXEMPTIONS_FIELD,
|
|
11
|
-
)
|
|
12
|
-
from repository_checks.policy_document import (
|
|
13
|
-
PolicyConfigurationRunFatal,
|
|
14
|
-
entry_path_and_digest,
|
|
15
|
-
read_policy_entries,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def load_email_exemptions(repository_root: Path) -> frozenset[tuple[str, str, str]]:
|
|
20
|
-
"""Read validated email exceptions.
|
|
21
|
-
|
|
22
|
-
Args:
|
|
23
|
-
repository_root: Repository containing the optional policy configuration.
|
|
24
|
-
Returns:
|
|
25
|
-
Exact path, email category, and digest identities.
|
|
26
|
-
Raises:
|
|
27
|
-
ValueError: Configuration contains invalid JSON or schema fields.
|
|
28
|
-
OSError: Configuration cannot be read.
|
|
29
|
-
"""
|
|
30
|
-
exemptions: set[tuple[str, str, str]] = set()
|
|
31
|
-
for each_entry in read_policy_entries(repository_root, EMAIL_EXEMPTIONS_FIELD):
|
|
32
|
-
relative_path, digest = entry_path_and_digest(
|
|
33
|
-
each_entry, EMAIL_EXEMPTION_SUBJECT
|
|
34
|
-
)
|
|
35
|
-
identity = (relative_path, EMAIL_CATEGORY, digest)
|
|
36
|
-
if identity in exemptions:
|
|
37
|
-
raise PolicyConfigurationRunFatal("Duplicate repository email exemption")
|
|
38
|
-
exemptions.add(identity)
|
|
39
|
-
return frozenset(exemptions)
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
"""Exact public-contact exceptions preserve all other scanner findings."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import hashlib
|
|
6
|
-
import json
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
import pytest
|
|
10
|
-
from repository_checks.email_exemptions import load_email_exemptions
|
|
11
|
-
from repository_checks.tracked_secrets import collect_tracked_secret_findings
|
|
12
|
-
|
|
13
|
-
CONTACT = "fixture-contact@company.io"
|
|
14
|
-
CHANGED_CONTACT = "changed-contact@company.io"
|
|
15
|
-
DIGEST = hashlib.sha256(CONTACT.encode()).hexdigest()
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _write_config(root: Path, entries: list[object]) -> None:
|
|
19
|
-
directory = root / "config"
|
|
20
|
-
directory.mkdir(exist_ok=True)
|
|
21
|
-
(directory / "repository-policy.json").write_text(
|
|
22
|
-
json.dumps({"version": 1, "email_exemptions": entries}), encoding="utf-8"
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _entry(**changes: object) -> dict[str, object]:
|
|
27
|
-
return {
|
|
28
|
-
"path": "contacts.py",
|
|
29
|
-
"sha256": DIGEST,
|
|
30
|
-
"reason": "Published support contact",
|
|
31
|
-
**changes,
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def test_only_matching_email_and_path_are_exempt(tmp_path: Path) -> None:
|
|
36
|
-
_write_config(tmp_path, [_entry()])
|
|
37
|
-
(tmp_path / "contacts.py").write_text(
|
|
38
|
-
f"contact = '{CONTACT}'\nchanged = '{CHANGED_CONTACT}'\n", encoding="utf-8"
|
|
39
|
-
)
|
|
40
|
-
(tmp_path / "other.py").write_text(f"contact = '{CONTACT}'\n", encoding="utf-8")
|
|
41
|
-
findings = collect_tracked_secret_findings(tmp_path, ["contacts.py", "other.py"])
|
|
42
|
-
assert [finding.relative_path for finding in findings] == [
|
|
43
|
-
"contacts.py",
|
|
44
|
-
"other.py",
|
|
45
|
-
]
|
|
46
|
-
assert all(finding.message.startswith("[email]") for finding in findings)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def test_matching_contact_has_no_findings(tmp_path: Path) -> None:
|
|
50
|
-
_write_config(tmp_path, [_entry()])
|
|
51
|
-
(tmp_path / "contacts.py").write_text(f"contact = '{CONTACT}'\n", encoding="utf-8")
|
|
52
|
-
assert collect_tracked_secret_findings(tmp_path, ["contacts.py"]) == []
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@pytest.mark.parametrize(
|
|
56
|
-
"path",
|
|
57
|
-
[
|
|
58
|
-
"/contacts.py",
|
|
59
|
-
"C:/contacts.py",
|
|
60
|
-
"../contacts.py",
|
|
61
|
-
"src/../contacts.py",
|
|
62
|
-
"*.py",
|
|
63
|
-
"src/?.py",
|
|
64
|
-
"src/[a].py",
|
|
65
|
-
"src\\contacts.py",
|
|
66
|
-
"src//contacts.py",
|
|
67
|
-
"./contacts.py",
|
|
68
|
-
"",
|
|
69
|
-
],
|
|
70
|
-
)
|
|
71
|
-
def test_rejects_nonliteral_or_nonrelative_paths(tmp_path: Path, path: str) -> None:
|
|
72
|
-
_write_config(tmp_path, [_entry(path=path)])
|
|
73
|
-
with pytest.raises(ValueError):
|
|
74
|
-
load_email_exemptions(tmp_path)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
@pytest.mark.parametrize(
|
|
78
|
-
"entry",
|
|
79
|
-
[
|
|
80
|
-
None,
|
|
81
|
-
{},
|
|
82
|
-
_entry(reason=" "),
|
|
83
|
-
_entry(sha256="f" * 63),
|
|
84
|
-
_entry(sha256="z" * 64),
|
|
85
|
-
_entry(category="secret"),
|
|
86
|
-
],
|
|
87
|
-
)
|
|
88
|
-
def test_rejects_invalid_entries(tmp_path: Path, entry: object) -> None:
|
|
89
|
-
_write_config(tmp_path, [entry])
|
|
90
|
-
with pytest.raises(ValueError):
|
|
91
|
-
load_email_exemptions(tmp_path)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def test_rejects_duplicate_entries(tmp_path: Path) -> None:
|
|
95
|
-
_write_config(tmp_path, [_entry(), _entry(reason="Another explanation")])
|
|
96
|
-
with pytest.raises(ValueError):
|
|
97
|
-
load_email_exemptions(tmp_path)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
@pytest.mark.parametrize(
|
|
101
|
-
"content",
|
|
102
|
-
[
|
|
103
|
-
"{",
|
|
104
|
-
"[]",
|
|
105
|
-
'{"version":true,"email_exemptions":[]}',
|
|
106
|
-
'{"version":1,"email_exemptions":{}}',
|
|
107
|
-
'{"version":1,"email_exemptions":[],"extra":0}',
|
|
108
|
-
'{"version":1,"version":1,"email_exemptions":[]}',
|
|
109
|
-
],
|
|
110
|
-
)
|
|
111
|
-
def test_malformed_config_stops_scanning(tmp_path: Path, content: str) -> None:
|
|
112
|
-
(tmp_path / "config").mkdir()
|
|
113
|
-
(tmp_path / "config" / "repository-policy.json").write_text(
|
|
114
|
-
content, encoding="utf-8"
|
|
115
|
-
)
|
|
116
|
-
with pytest.raises(ValueError):
|
|
117
|
-
collect_tracked_secret_findings(tmp_path, [])
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def test_missing_config_has_no_exemptions(tmp_path: Path) -> None:
|
|
121
|
-
assert load_email_exemptions(tmp_path) == frozenset()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
@pytest.mark.parametrize(
|
|
125
|
-
"document",
|
|
126
|
-
[
|
|
127
|
-
{"version": 1},
|
|
128
|
-
{
|
|
129
|
-
"version": 1,
|
|
130
|
-
"path_exemptions": [
|
|
131
|
-
{
|
|
132
|
-
"path": "evidence/manifest.json",
|
|
133
|
-
"sha256": DIGEST,
|
|
134
|
-
"reason": "Frozen evidence file pinned by its consumers",
|
|
135
|
-
}
|
|
136
|
-
],
|
|
137
|
-
},
|
|
138
|
-
],
|
|
139
|
-
)
|
|
140
|
-
def test_document_without_the_email_list_loads_no_email_exemptions(
|
|
141
|
-
tmp_path: Path, document: dict[str, object]
|
|
142
|
-
) -> None:
|
|
143
|
-
"""A document may carry the other family, or no exception list at all."""
|
|
144
|
-
(tmp_path / "config").mkdir()
|
|
145
|
-
(tmp_path / "config" / "repository-policy.json").write_text(
|
|
146
|
-
json.dumps(document), encoding="utf-8"
|
|
147
|
-
)
|
|
148
|
-
assert load_email_exemptions(tmp_path) == frozenset()
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def test_reason_rejection_names_the_ownership_requirement(tmp_path: Path) -> None:
|
|
152
|
-
_write_config(tmp_path, [_entry(reason=" ")])
|
|
153
|
-
with pytest.raises(ValueError) as rejection:
|
|
154
|
-
load_email_exemptions(tmp_path)
|
|
155
|
-
assert (
|
|
156
|
-
str(rejection.value)
|
|
157
|
-
== "Email exemption reason must explain why the exception is owned"
|
|
158
|
-
)
|