Skip to content

Commit d7bc034

Browse files
leliaclaude
andcommitted
fix(enums): keep ScanType strict, it only ever builds requests
ScanType never parses an API response. FullScanParams.to_dict() is urlencoded onto the create-scan query string, so giving it a _missing_ fallback meant a caller typo silently shipped scan_type=unknown to the API instead of failing at construction. The same from_dict already passes integration_type through uncoerced for that reason. It is now recorded in REQUEST_ONLY_ENUMS, the opt-out the invariant test always had and this branch had left empty, and a new test asserts request-only enums keep raising so the exemption cannot quietly become a skip. Also bumps actions/setup-python in the new workflow to v7.0.0, matching the pin already used by .github/actions/setup-sfw. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 203b5e8 commit d7bc034

5 files changed

Lines changed: 50 additions & 13 deletions

File tree

.github/workflows/api-drift-check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
fetch-depth: 1
2727
persist-credentials: false
2828

29-
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
29+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
3030
with:
3131
python-version: "3.12"
3232

CHANGELOG.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
### Changed: every API-sourced enum now tolerates unknown values
66

7-
- `SocketIssueSeverity`, `SocketCategory`, `DiffType`, `ScanType` and
8-
`SecurityAction` now fall back to a documented member instead of raising
7+
- `SocketIssueSeverity`, `SocketCategory`, `DiffType` and `SecurityAction`
8+
now fall back to a documented member instead of raising
99
`ValueError` when the API sends a value this release does not know about.
10-
`SocketPURL_Type` already behaved this way; the other five did not, so each
11-
was one backend addition away from emptying a response the same way issue #78
12-
and the unknown `generic` purl type did.
10+
`SocketPURL_Type` already behaved this way; the others did not, so each was
11+
one backend addition away from emptying a response the same way issue #78 and
12+
the unknown `generic` purl type did. `ScanType` is deliberately left strict:
13+
it is only ever urlencoded onto the create-scan request, so an unrecognized
14+
value is a caller typo rather than API drift.
1315
- Fallbacks are deliberate rather than convenient. `SocketIssueSeverity` and
1416
`DiffType` gained an explicit `UNKNOWN` member because guessing an existing
1517
level would either hide a real finding or invent one, and `SecurityAction`

scripts/check_api_enum_drift.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,14 @@
4343
SocketIssueSeverity: "SocketIssueSeverity",
4444
SocketCategory: "SocketCategory",
4545
DiffType: "SocketDiffArtifactType",
46-
ScanType: None,
4746
SecurityAction: None,
4847
}
4948

49+
# Enums the SDK only ever sends, never parses. Drift in the API's copy cannot
50+
# break parsing here, and these are intentionally strict so a caller typo fails
51+
# at construction, so they are out of scope for this check rather than a gap.
52+
REQUEST_ONLY = (ScanType,)
53+
5054
# Members this SDK adds deliberately, which the API will never send. They are
5155
# the documented _missing_ fallbacks (see socketdev/core/enums.py), so their
5256
# absence from the spec is expected rather than drift.
@@ -122,6 +126,11 @@ def main():
122126
f"named schema for them: {', '.join(sorted(unmapped))}"
123127
)
124128

129+
print(
130+
f"not applicable (request-only, intentionally strict): "
131+
f"{', '.join(sorted(e.__name__ for e in REQUEST_ONLY))}"
132+
)
133+
125134
if drifted:
126135
print(
127136
"\nAdd the missing members to the SDK enum. Existing values are "

socketdev/fullscans/__init__.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,14 @@ def _missing_(cls, value):
102102

103103

104104
class ScanType(str, Enum):
105+
# Deliberately strict, unlike the response-parsed enums in this module.
106+
# ScanType only ever travels outbound: FullScanParams.to_dict() is
107+
# urlencoded onto the create-scan query string, so an unrecognized value is
108+
# the caller's typo, not API drift. Coercing it to a fallback would send
109+
# scan_type=unknown to the API instead of failing at construction.
105110
SOCKET = "socket"
106111
SOCKET_TIER1 = "socket_tier1"
107112
SOCKET_BASICS = "socket_basics"
108-
UNKNOWN = "unknown"
109-
110-
@classmethod
111-
def _missing_(cls, value):
112-
return unknown_enum_value(cls.__name__, value, cls.UNKNOWN)
113113

114114

115115
@dataclass(kw_only=True)

tests/unit/test_enum_forward_compat.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,15 @@
2525
# Strictness is correct there: a bad value is the caller's typo and should raise
2626
# rather than be silently coerced. Add an entry only with a comment justifying
2727
# that the enum never sees API-supplied values.
28-
REQUEST_ONLY_ENUMS = frozenset()
28+
REQUEST_ONLY_ENUMS = frozenset(
29+
{
30+
# Only ever travels outbound: FullScanParams.to_dict() is urlencoded
31+
# onto the create-scan query string. It never parses an API response, so
32+
# an unrecognized value is a caller typo that should surface at
33+
# construction rather than reach the API as scan_type=unknown.
34+
"ScanType",
35+
}
36+
)
2937

3038
# A value the API will never legitimately send.
3139
SENTINEL = "__value_the_api_would_never_send__"
@@ -98,6 +106,24 @@ def test_unknown_value_warns(self):
98106
f"got: {captured.output}",
99107
)
100108

109+
def test_request_only_enums_stay_strict(self):
110+
# The opt-out is not a "skip this one" marker: these enums must actively
111+
# keep raising. Coercing a caller's typo to a fallback would send the
112+
# fallback to the API instead of failing at construction, which is how
113+
# the forward-compat change first got ScanType wrong.
114+
by_name = {cls.__name__: cls for cls in _all_enums().values()}
115+
for name in sorted(REQUEST_ONLY_ENUMS):
116+
with self.subTest(enum=name):
117+
enum_cls = by_name.get(name)
118+
self.assertIsNotNone(
119+
enum_cls, f"{name} is exempted but no longer exists"
120+
)
121+
with self.assertRaises(
122+
ValueError,
123+
msg=f"{name} is request-only and must reject unknown values",
124+
):
125+
enum_cls(SENTINEL)
126+
101127
def test_known_values_still_round_trip(self):
102128
# Forward-compat must not swallow legitimate values.
103129
for qualname, enum_cls in sorted(_all_enums().items()):

0 commit comments

Comments
 (0)