claude-dev-env 7.4.0 → 7.4.1
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/bin/install.test.mjs +4 -3
- package/bin/pstack.mjs +24 -4
- package/bin/pstack.test.mjs +20 -10
- package/hooks/blocking/env_var_table_code_drift_blocker.py +11 -99
- package/hooks/blocking/env_var_table_rows.py +130 -0
- package/hooks/blocking/pii_scanner.py +19 -29
- package/hooks/blocking/test_env_var_table_code_drift_blocker.py +81 -3
- package/hooks/blocking/test_env_var_table_rows.py +100 -0
- package/hooks/blocking/test_pii_scanner.py +34 -24
- package/hooks/hooks_constants/env_var_table_code_drift_constants.py +12 -4
- package/hooks/hooks_constants/hardcoded_user_path_constants.py +12 -10
- package/hooks/hooks_constants/pii_prevention_constants.py +10 -25
- package/package.json +1 -1
- package/scripts/pstack-adapters/common.md +1 -1
- package/scripts/repository_checks/config/constants.py +7 -17
- package/scripts/repository_checks/config/email_exemptions.py +10 -0
- package/scripts/repository_checks/email_exemptions.py +96 -0
- package/scripts/repository_checks/env_var_documentation.py +1 -9
- package/scripts/repository_checks/tracked_secrets.py +10 -4
- package/scripts/tests/test_email_exemptions.py +121 -0
- package/scripts/tests/test_env_var_documentation.py +26 -0
- package/scripts/tests/test_tracked_secrets.py +13 -0
|
@@ -0,0 +1,121 @@
|
|
|
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()
|
|
@@ -17,6 +17,9 @@ from repository_checks.config.constants import (
|
|
|
17
17
|
FINDINGS_EXIT_CODE,
|
|
18
18
|
SUCCESS_EXIT_CODE,
|
|
19
19
|
)
|
|
20
|
+
from repository_checks.env_var_documentation import (
|
|
21
|
+
collect_env_var_documentation_findings,
|
|
22
|
+
)
|
|
20
23
|
from repository_policy_test_support import (
|
|
21
24
|
commit_tracked_files,
|
|
22
25
|
initialize_repository,
|
|
@@ -131,3 +134,26 @@ def record_read_paths(monkeypatch: pytest.MonkeyPatch) -> list[Path]:
|
|
|
131
134
|
|
|
132
135
|
monkeypatch.setattr(Path, "read_text", record_read_path)
|
|
133
136
|
return all_read_paths
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@pytest.mark.parametrize("header", ["Symbol", "Component", "Constant"])
|
|
140
|
+
def test_should_ignore_source_tables_before_reading_named_code(
|
|
141
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, header: str
|
|
142
|
+
) -> None:
|
|
143
|
+
repository_root = tmp_path / "repo"
|
|
144
|
+
initialize_repository(repository_root)
|
|
145
|
+
write_text(repository_root / "workflow.py", "def run():\n return\n")
|
|
146
|
+
write_text(
|
|
147
|
+
repository_root / "docs" / "source-map.md",
|
|
148
|
+
f"| {header} | Source |\n"
|
|
149
|
+
"| --- | --- |\n"
|
|
150
|
+
"| `PROCESSING_VISIBLE` | `workflow.py` |\n",
|
|
151
|
+
)
|
|
152
|
+
commit_tracked_files(repository_root)
|
|
153
|
+
patch_unreadable_named_file(
|
|
154
|
+
monkeypatch, "workflow.py", "unrelated source unreadable"
|
|
155
|
+
)
|
|
156
|
+
assert (
|
|
157
|
+
collect_env_var_documentation_findings(repository_root, ["docs/source-map.md"])
|
|
158
|
+
== []
|
|
159
|
+
)
|
|
@@ -10,6 +10,7 @@ import pytest
|
|
|
10
10
|
from repository_checks.config.constants import (
|
|
11
11
|
ALL_TRACKED_SECRET_EXACT_EXEMPTIONS,
|
|
12
12
|
CHECK_ID_TRACKED_PERSONAL_DATA,
|
|
13
|
+
FAILED_CHECK_EXIT_CODE,
|
|
13
14
|
FINDINGS_EXIT_CODE,
|
|
14
15
|
)
|
|
15
16
|
from repository_checks.hook_modules import load_hooks_module
|
|
@@ -55,6 +56,18 @@ def test_should_flag_a_tracked_secret(tmp_path: Path) -> None:
|
|
|
55
56
|
assert _SYNTHETIC_GITHUB_TOKEN not in stdout_text
|
|
56
57
|
|
|
57
58
|
|
|
59
|
+
def test_should_fail_closed_for_malformed_repository_email_policy(
|
|
60
|
+
tmp_path: Path,
|
|
61
|
+
) -> None:
|
|
62
|
+
repository_root = tmp_path / "repo"
|
|
63
|
+
initialize_repository(repository_root)
|
|
64
|
+
write_text(repository_root / "config" / "repository-policy.json", "{")
|
|
65
|
+
commit_tracked_files(repository_root)
|
|
66
|
+
exit_code, stdout_text, _stderr_text = run_policy(repository_root)
|
|
67
|
+
assert exit_code == FAILED_CHECK_EXIT_CODE
|
|
68
|
+
assert f"error: rule failed: {CHECK_ID_TRACKED_PERSONAL_DATA}" in stdout_text
|
|
69
|
+
|
|
70
|
+
|
|
58
71
|
def test_should_fail_closed_before_reading_a_tracked_symlink_outside_repository(
|
|
59
72
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
60
73
|
) -> None:
|