zod-openapi-contract-lint-kit 0.1.1__tar.gz
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.
- zod_openapi_contract_lint_kit-0.1.1/LICENSE +21 -0
- zod_openapi_contract_lint_kit-0.1.1/PKG-INFO +80 -0
- zod_openapi_contract_lint_kit-0.1.1/README.md +56 -0
- zod_openapi_contract_lint_kit-0.1.1/pyproject.toml +39 -0
- zod_openapi_contract_lint_kit-0.1.1/setup.cfg +4 -0
- zod_openapi_contract_lint_kit-0.1.1/src/zod_openapi_contract_lint_kit/__init__.py +3 -0
- zod_openapi_contract_lint_kit-0.1.1/src/zod_openapi_contract_lint_kit/cli.py +271 -0
- zod_openapi_contract_lint_kit-0.1.1/src/zod_openapi_contract_lint_kit/rules.json +64 -0
- zod_openapi_contract_lint_kit-0.1.1/src/zod_openapi_contract_lint_kit.egg-info/PKG-INFO +80 -0
- zod_openapi_contract_lint_kit-0.1.1/src/zod_openapi_contract_lint_kit.egg-info/SOURCES.txt +12 -0
- zod_openapi_contract_lint_kit-0.1.1/src/zod_openapi_contract_lint_kit.egg-info/dependency_links.txt +1 -0
- zod_openapi_contract_lint_kit-0.1.1/src/zod_openapi_contract_lint_kit.egg-info/entry_points.txt +3 -0
- zod_openapi_contract_lint_kit-0.1.1/src/zod_openapi_contract_lint_kit.egg-info/top_level.txt +1 -0
- zod_openapi_contract_lint_kit-0.1.1/tests/test_scanner.py +99 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zod-openapi-contract-lint-kit
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Local read-only scanner for Zod/OpenAPI contract drift risks.
|
|
5
|
+
Author: Zod OpenAPI Contract Lint Kit contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vasiliy0/zod-openapi-contract-lint-kit
|
|
8
|
+
Project-URL: Repository, https://github.com/vasiliy0/zod-openapi-contract-lint-kit
|
|
9
|
+
Project-URL: Issues, https://github.com/vasiliy0/zod-openapi-contract-lint-kit/issues
|
|
10
|
+
Keywords: zod,openapi,typescript,api,lint
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# Zod OpenAPI Contract Lint Kit
|
|
26
|
+
|
|
27
|
+
Local read-only prototype for finding common drift risks between Zod schemas, OpenAPI metadata, route responses, and generated API clients.
|
|
28
|
+
|
|
29
|
+
## Why this exists
|
|
30
|
+
|
|
31
|
+
Teams often start with Zod as runtime validation and later generate OpenAPI specs/clients. Drift appears when schemas use constructs generators cannot represent cleanly, when route metadata is missing, or when auth/error responses are inconsistent.
|
|
32
|
+
|
|
33
|
+
This prototype scans TypeScript files and reports review items. It does not execute application code, import project modules, call network APIs, or modify files.
|
|
34
|
+
|
|
35
|
+
## Current v1 checks
|
|
36
|
+
|
|
37
|
+
- Zod object schemas exported without nearby OpenAPI metadata (`.openapi(...)` or `.describe(...)`).
|
|
38
|
+
- `z.any()` / `z.unknown()` in public contracts.
|
|
39
|
+
- `z.date()` fields that may serialize differently in OpenAPI/client transports.
|
|
40
|
+
- `z.record(...)` map types that may need `additionalProperties` review.
|
|
41
|
+
- `z.union(...)` without a nearby discriminator hint.
|
|
42
|
+
- `z.transform(...)`, `.refine(...)`, `.superRefine(...)` in public schemas.
|
|
43
|
+
- Route handlers with auth hints but no visible `401` / `403` response docs nearby.
|
|
44
|
+
- Error responses without an obvious schema/envelope reference.
|
|
45
|
+
|
|
46
|
+
## Try locally
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python3 scanner.py examples
|
|
50
|
+
python3 scanner.py examples --format json
|
|
51
|
+
python3 scanner.py examples --output report.md
|
|
52
|
+
python3 scanner.py examples --fail-on-severity high
|
|
53
|
+
python3 scanner.py examples --min-severity high
|
|
54
|
+
python3 scanner.py examples --only-rule zod-date-serialization-review
|
|
55
|
+
python3 scanner.py --list-rules
|
|
56
|
+
python3 scanner.py --list-rules --format json
|
|
57
|
+
python3 scanner.py examples --ignore-rule runtime-only-zod-effect
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## CI usage
|
|
61
|
+
|
|
62
|
+
See [`docs/CI_USAGE.md`](docs/CI_USAGE.md) for report-only, high-risk-only, and scoped rollout examples.
|
|
63
|
+
|
|
64
|
+
## Safety
|
|
65
|
+
|
|
66
|
+
- Read-only local scan.
|
|
67
|
+
- No GitHub/npm/OpenAPI service account required.
|
|
68
|
+
- No dependency installation required for the prototype.
|
|
69
|
+
- Findings are review prompts, not automatic migration claims.
|
|
70
|
+
- CI failure is opt-in via `--fail-on-severity`.
|
|
71
|
+
- Rule filtering is explicit and local; unknown rule ids fail fast instead of silently changing coverage.
|
|
72
|
+
- `--min-severity` supports high-risk-only reports for CI summaries.
|
|
73
|
+
- `--list-rules` can be used to review active rule coverage before adding the scanner to CI.
|
|
74
|
+
|
|
75
|
+
## Roadmap
|
|
76
|
+
|
|
77
|
+
- Expand fixtures across common Zod/OpenAPI routing patterns.
|
|
78
|
+
- Add fixtures for hono/zod-openapi/express-zod-api route variants.
|
|
79
|
+
- Package as a small developer CLI after the prototype stabilizes.
|
|
80
|
+
- Add more routing-library examples and release notes.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Zod OpenAPI Contract Lint Kit
|
|
2
|
+
|
|
3
|
+
Local read-only prototype for finding common drift risks between Zod schemas, OpenAPI metadata, route responses, and generated API clients.
|
|
4
|
+
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
Teams often start with Zod as runtime validation and later generate OpenAPI specs/clients. Drift appears when schemas use constructs generators cannot represent cleanly, when route metadata is missing, or when auth/error responses are inconsistent.
|
|
8
|
+
|
|
9
|
+
This prototype scans TypeScript files and reports review items. It does not execute application code, import project modules, call network APIs, or modify files.
|
|
10
|
+
|
|
11
|
+
## Current v1 checks
|
|
12
|
+
|
|
13
|
+
- Zod object schemas exported without nearby OpenAPI metadata (`.openapi(...)` or `.describe(...)`).
|
|
14
|
+
- `z.any()` / `z.unknown()` in public contracts.
|
|
15
|
+
- `z.date()` fields that may serialize differently in OpenAPI/client transports.
|
|
16
|
+
- `z.record(...)` map types that may need `additionalProperties` review.
|
|
17
|
+
- `z.union(...)` without a nearby discriminator hint.
|
|
18
|
+
- `z.transform(...)`, `.refine(...)`, `.superRefine(...)` in public schemas.
|
|
19
|
+
- Route handlers with auth hints but no visible `401` / `403` response docs nearby.
|
|
20
|
+
- Error responses without an obvious schema/envelope reference.
|
|
21
|
+
|
|
22
|
+
## Try locally
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python3 scanner.py examples
|
|
26
|
+
python3 scanner.py examples --format json
|
|
27
|
+
python3 scanner.py examples --output report.md
|
|
28
|
+
python3 scanner.py examples --fail-on-severity high
|
|
29
|
+
python3 scanner.py examples --min-severity high
|
|
30
|
+
python3 scanner.py examples --only-rule zod-date-serialization-review
|
|
31
|
+
python3 scanner.py --list-rules
|
|
32
|
+
python3 scanner.py --list-rules --format json
|
|
33
|
+
python3 scanner.py examples --ignore-rule runtime-only-zod-effect
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## CI usage
|
|
37
|
+
|
|
38
|
+
See [`docs/CI_USAGE.md`](docs/CI_USAGE.md) for report-only, high-risk-only, and scoped rollout examples.
|
|
39
|
+
|
|
40
|
+
## Safety
|
|
41
|
+
|
|
42
|
+
- Read-only local scan.
|
|
43
|
+
- No GitHub/npm/OpenAPI service account required.
|
|
44
|
+
- No dependency installation required for the prototype.
|
|
45
|
+
- Findings are review prompts, not automatic migration claims.
|
|
46
|
+
- CI failure is opt-in via `--fail-on-severity`.
|
|
47
|
+
- Rule filtering is explicit and local; unknown rule ids fail fast instead of silently changing coverage.
|
|
48
|
+
- `--min-severity` supports high-risk-only reports for CI summaries.
|
|
49
|
+
- `--list-rules` can be used to review active rule coverage before adding the scanner to CI.
|
|
50
|
+
|
|
51
|
+
## Roadmap
|
|
52
|
+
|
|
53
|
+
- Expand fixtures across common Zod/OpenAPI routing patterns.
|
|
54
|
+
- Add fixtures for hono/zod-openapi/express-zod-api route variants.
|
|
55
|
+
- Package as a small developer CLI after the prototype stabilizes.
|
|
56
|
+
- Add more routing-library examples and release notes.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zod-openapi-contract-lint-kit"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Local read-only scanner for Zod/OpenAPI contract drift risks."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Zod OpenAPI Contract Lint Kit contributors" }]
|
|
13
|
+
keywords = ["zod", "openapi", "typescript", "api", "lint"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Software Development :: Code Generators",
|
|
23
|
+
"Topic :: Software Development :: Quality Assurance"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/vasiliy0/zod-openapi-contract-lint-kit"
|
|
28
|
+
Repository = "https://github.com/vasiliy0/zod-openapi-contract-lint-kit"
|
|
29
|
+
Issues = "https://github.com/vasiliy0/zod-openapi-contract-lint-kit/issues"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
zod-openapi-contract-lint-kit = "zod_openapi_contract_lint_kit.cli:main"
|
|
33
|
+
zod-openapi-lint = "zod_openapi_contract_lint_kit.cli:main"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["src"]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.package-data]
|
|
39
|
+
zod_openapi_contract_lint_kit = ["rules.json"]
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read-only static scanner for Zod/OpenAPI contract drift risks."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import asdict, dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Iterable
|
|
11
|
+
|
|
12
|
+
DEFAULT_RULES = Path(__file__).with_name("rules.json")
|
|
13
|
+
SUPPORTED_SUFFIXES = {".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"}
|
|
14
|
+
SEVERITY_ORDER = {"low": 1, "medium": 2, "high": 3}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class Rule:
|
|
19
|
+
id: str
|
|
20
|
+
severity: str
|
|
21
|
+
pattern: str
|
|
22
|
+
why: str
|
|
23
|
+
fix: str
|
|
24
|
+
suppress_if_nearby: tuple[str, ...] = ()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class Finding:
|
|
29
|
+
file: str
|
|
30
|
+
line: int
|
|
31
|
+
rule_id: str
|
|
32
|
+
severity: str
|
|
33
|
+
signal: str
|
|
34
|
+
why: str
|
|
35
|
+
fix: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_rules(path: Path = DEFAULT_RULES) -> list[Rule]:
|
|
39
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
40
|
+
rules = []
|
|
41
|
+
for item in data["rules"]:
|
|
42
|
+
rules.append(Rule(
|
|
43
|
+
id=item["id"],
|
|
44
|
+
severity=item["severity"],
|
|
45
|
+
pattern=item["pattern"],
|
|
46
|
+
why=item["why"],
|
|
47
|
+
fix=item["fix"],
|
|
48
|
+
suppress_if_nearby=tuple(item.get("suppress_if_nearby", [])),
|
|
49
|
+
))
|
|
50
|
+
return rules
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def filter_rules(rules: Iterable[Rule], only_rule: set[str] | None = None, ignore_rule: set[str] | None = None) -> list[Rule]:
|
|
54
|
+
only_rule = only_rule or set()
|
|
55
|
+
ignore_rule = ignore_rule or set()
|
|
56
|
+
known_ids = {rule.id for rule in rules}
|
|
57
|
+
unknown = (only_rule | ignore_rule) - known_ids
|
|
58
|
+
if unknown:
|
|
59
|
+
raise ValueError("Unknown rule id(s): " + ", ".join(sorted(unknown)))
|
|
60
|
+
filtered = []
|
|
61
|
+
for rule in rules:
|
|
62
|
+
if only_rule and rule.id not in only_rule:
|
|
63
|
+
continue
|
|
64
|
+
if rule.id in ignore_rule:
|
|
65
|
+
continue
|
|
66
|
+
filtered.append(rule)
|
|
67
|
+
return filtered
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def discover(root: Path) -> list[Path]:
|
|
71
|
+
if root.is_file():
|
|
72
|
+
return [root] if root.suffix.lower() in SUPPORTED_SUFFIXES else []
|
|
73
|
+
ignored = {"node_modules", "dist", "build", ".git", ".next", "coverage"}
|
|
74
|
+
files = []
|
|
75
|
+
for path in root.rglob("*"):
|
|
76
|
+
if not path.is_file() or path.suffix.lower() not in SUPPORTED_SUFFIXES:
|
|
77
|
+
continue
|
|
78
|
+
if any(part in ignored for part in path.parts):
|
|
79
|
+
continue
|
|
80
|
+
files.append(path)
|
|
81
|
+
return sorted(files)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def nearby_text(lines: list[str], index: int, radius: int = 4) -> str:
|
|
85
|
+
start = max(0, index - radius)
|
|
86
|
+
end = min(len(lines), index + radius + 1)
|
|
87
|
+
return "\n".join(lines[start:end])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def is_suppressed(rule: Rule, lines: list[str], index: int) -> bool:
|
|
91
|
+
if not rule.suppress_if_nearby:
|
|
92
|
+
return False
|
|
93
|
+
context = nearby_text(lines, index)
|
|
94
|
+
return any(token in context for token in rule.suppress_if_nearby)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def display_path(path: Path, root: Path) -> str:
|
|
98
|
+
try:
|
|
99
|
+
return str(path.relative_to(root))
|
|
100
|
+
except ValueError:
|
|
101
|
+
return str(path)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def scan_file(path: Path, root: Path, rules: list[Rule]) -> list[Finding]:
|
|
105
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
106
|
+
lines = text.splitlines()
|
|
107
|
+
findings: list[Finding] = []
|
|
108
|
+
for i, line in enumerate(lines):
|
|
109
|
+
for rule in rules:
|
|
110
|
+
if re.search(rule.pattern, line, flags=re.I):
|
|
111
|
+
if is_suppressed(rule, lines, i):
|
|
112
|
+
continue
|
|
113
|
+
findings.append(Finding(
|
|
114
|
+
file=display_path(path, root),
|
|
115
|
+
line=i + 1,
|
|
116
|
+
rule_id=rule.id,
|
|
117
|
+
severity=rule.severity,
|
|
118
|
+
signal=line.strip(),
|
|
119
|
+
why=rule.why,
|
|
120
|
+
fix=rule.fix,
|
|
121
|
+
))
|
|
122
|
+
return findings
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def scan(root: Path, rules_path: Path = DEFAULT_RULES, only_rule: set[str] | None = None, ignore_rule: set[str] | None = None) -> dict[str, Any]:
|
|
126
|
+
root = root.resolve()
|
|
127
|
+
base = root if root.is_dir() else root.parent
|
|
128
|
+
rules = filter_rules(load_rules(rules_path), only_rule=only_rule, ignore_rule=ignore_rule)
|
|
129
|
+
files = discover(root)
|
|
130
|
+
findings: list[Finding] = []
|
|
131
|
+
for file in files:
|
|
132
|
+
findings.extend(scan_file(file, base, rules))
|
|
133
|
+
return {
|
|
134
|
+
"scanned_files": len(files),
|
|
135
|
+
"active_rule_count": len(rules),
|
|
136
|
+
"finding_count": len(findings),
|
|
137
|
+
"findings": [asdict(f) for f in findings],
|
|
138
|
+
"summary_by_severity": severity_summary(findings),
|
|
139
|
+
"summary_by_rule": rule_summary(findings),
|
|
140
|
+
"notes": [
|
|
141
|
+
"Read-only static scan; no TypeScript execution, imports, network calls, or file changes.",
|
|
142
|
+
"Findings are contract-review prompts and may need project-specific suppression rules.",
|
|
143
|
+
],
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def severity_summary(findings: list[Finding]) -> dict[str, int]:
|
|
148
|
+
summary: dict[str, int] = {}
|
|
149
|
+
for finding in findings:
|
|
150
|
+
summary[finding.severity] = summary.get(finding.severity, 0) + 1
|
|
151
|
+
return summary
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def rule_summary(findings: list[Finding]) -> dict[str, int]:
|
|
155
|
+
summary: dict[str, int] = {}
|
|
156
|
+
for finding in findings:
|
|
157
|
+
summary[finding.rule_id] = summary.get(finding.rule_id, 0) + 1
|
|
158
|
+
return summary
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def render_markdown(report: dict[str, Any]) -> str:
|
|
162
|
+
lines = ["# Zod OpenAPI Contract Lint Report", ""]
|
|
163
|
+
lines.append(f"Scanned files: {report['scanned_files']}")
|
|
164
|
+
lines.append(f"Active rules: {report.get('active_rule_count', 'n/a')}")
|
|
165
|
+
lines.append(f"Findings: {report['finding_count']}")
|
|
166
|
+
if report["summary_by_severity"]:
|
|
167
|
+
lines.append("")
|
|
168
|
+
lines.append("## Summary by severity")
|
|
169
|
+
for severity, count in report["summary_by_severity"].items():
|
|
170
|
+
lines.append(f"- **{severity}**: {count}")
|
|
171
|
+
if report.get("summary_by_rule"):
|
|
172
|
+
lines.append("")
|
|
173
|
+
lines.append("## Summary by rule")
|
|
174
|
+
for rule_id, count in report["summary_by_rule"].items():
|
|
175
|
+
lines.append(f"- `{rule_id}`: {count}")
|
|
176
|
+
if report["findings"]:
|
|
177
|
+
lines.append("")
|
|
178
|
+
lines.append("## Findings")
|
|
179
|
+
for finding in report["findings"]:
|
|
180
|
+
lines.append(f"- **{finding['severity']}** `{finding['rule_id']}` in `{finding['file']}:{finding['line']}`")
|
|
181
|
+
lines.append(f" - Signal: `{finding['signal']}`")
|
|
182
|
+
lines.append(f" - Why: {finding['why']}")
|
|
183
|
+
lines.append(f" - Fix: {finding['fix']}")
|
|
184
|
+
else:
|
|
185
|
+
lines.append("")
|
|
186
|
+
lines.append("No known contract drift signals found by the current rule set.")
|
|
187
|
+
lines.append("")
|
|
188
|
+
lines.append("## Notes")
|
|
189
|
+
for note in report["notes"]:
|
|
190
|
+
lines.append(f"- {note}")
|
|
191
|
+
return "\n".join(lines) + "\n"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def should_fail(report: dict[str, Any], threshold: str) -> bool:
|
|
195
|
+
minimum = SEVERITY_ORDER[threshold]
|
|
196
|
+
return any(SEVERITY_ORDER.get(item["severity"], 0) >= minimum for item in report["findings"])
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def filter_findings_by_severity(findings: list[Finding], minimum_severity: str | None = None) -> list[Finding]:
|
|
200
|
+
if not minimum_severity:
|
|
201
|
+
return findings
|
|
202
|
+
minimum = SEVERITY_ORDER[minimum_severity]
|
|
203
|
+
return [finding for finding in findings if SEVERITY_ORDER.get(finding.severity, 0) >= minimum]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def apply_report_filters(report: dict[str, Any], minimum_severity: str | None = None) -> dict[str, Any]:
|
|
207
|
+
if not minimum_severity:
|
|
208
|
+
return report
|
|
209
|
+
filtered = filter_findings_by_severity([Finding(**item) for item in report["findings"]], minimum_severity)
|
|
210
|
+
notes = list(report["notes"])
|
|
211
|
+
notes.append(f"Filtered findings below {minimum_severity} severity.")
|
|
212
|
+
return {
|
|
213
|
+
**report,
|
|
214
|
+
"finding_count": len(filtered),
|
|
215
|
+
"findings": [asdict(finding) for finding in filtered],
|
|
216
|
+
"summary_by_severity": severity_summary(filtered),
|
|
217
|
+
"summary_by_rule": rule_summary(filtered),
|
|
218
|
+
"notes": notes,
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def render_rule_inventory(rules: list[Rule], output_format: str = "markdown") -> str:
|
|
223
|
+
if output_format == "json":
|
|
224
|
+
return json.dumps({"rules": [asdict(rule) for rule in rules]}, indent=2) + "\n"
|
|
225
|
+
lines = ["# Zod OpenAPI Contract Lint rule inventory", ""]
|
|
226
|
+
for rule in rules:
|
|
227
|
+
lines.append(f"## `{rule.id}`")
|
|
228
|
+
lines.append("")
|
|
229
|
+
lines.append(f"- Severity: **{rule.severity}**")
|
|
230
|
+
lines.append(f"- Pattern: `{rule.pattern}`")
|
|
231
|
+
if rule.suppress_if_nearby:
|
|
232
|
+
lines.append("- Suppressed near: " + ", ".join(f"`{token}`" for token in rule.suppress_if_nearby))
|
|
233
|
+
lines.append(f"- Why: {rule.why}")
|
|
234
|
+
lines.append(f"- Fix: {rule.fix}")
|
|
235
|
+
lines.append("")
|
|
236
|
+
return "\n".join(lines)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def main(argv: list[str] | None = None) -> int:
|
|
240
|
+
parser = argparse.ArgumentParser(description="Scan Zod/OpenAPI contract drift risks.")
|
|
241
|
+
parser.add_argument("path", nargs="?", default=".", help="Project root or TypeScript file to scan")
|
|
242
|
+
parser.add_argument("--format", choices=["markdown", "json"], default="markdown")
|
|
243
|
+
parser.add_argument("--output", "-o", help="Write report to a file instead of stdout")
|
|
244
|
+
parser.add_argument("--fail-on-severity", choices=["low", "medium", "high"], help="Exit 1 when findings at or above this severity are detected")
|
|
245
|
+
parser.add_argument("--min-severity", choices=["low", "medium", "high"], help="Only include findings at or above this severity in the report")
|
|
246
|
+
parser.add_argument("--only-rule", action="append", default=[], help="Run only this rule id; repeat for multiple rules")
|
|
247
|
+
parser.add_argument("--ignore-rule", action="append", default=[], help="Skip this rule id; repeat for multiple rules")
|
|
248
|
+
parser.add_argument("--list-rules", action="store_true", help="Print the active rule inventory and exit")
|
|
249
|
+
parser.add_argument("--rules", type=Path, default=DEFAULT_RULES)
|
|
250
|
+
args = parser.parse_args(argv)
|
|
251
|
+
try:
|
|
252
|
+
active_rules = filter_rules(load_rules(args.rules), only_rule=set(args.only_rule), ignore_rule=set(args.ignore_rule))
|
|
253
|
+
if args.list_rules:
|
|
254
|
+
print(render_rule_inventory(active_rules, args.format), end="")
|
|
255
|
+
return 0
|
|
256
|
+
report = scan(Path(args.path), args.rules, only_rule=set(args.only_rule), ignore_rule=set(args.ignore_rule))
|
|
257
|
+
except ValueError as exc:
|
|
258
|
+
parser.error(str(exc))
|
|
259
|
+
report = apply_report_filters(report, args.min_severity)
|
|
260
|
+
output = json.dumps(report, indent=2) if args.format == "json" else render_markdown(report)
|
|
261
|
+
if args.output:
|
|
262
|
+
Path(args.output).write_text(output + ("" if output.endswith("\n") else "\n"), encoding="utf-8")
|
|
263
|
+
else:
|
|
264
|
+
print(output, end="" if args.format == "markdown" else "\n")
|
|
265
|
+
if args.fail_on_severity and should_fail(report, args.fail_on_severity):
|
|
266
|
+
return 1
|
|
267
|
+
return 0
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
if __name__ == "__main__":
|
|
271
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rules": [
|
|
3
|
+
{
|
|
4
|
+
"id": "missing-openapi-metadata",
|
|
5
|
+
"severity": "high",
|
|
6
|
+
"pattern": "export\\s+const\\s+\\w+Schema\\s*=\\s*z\\.object\\(",
|
|
7
|
+
"why": "Exported Zod object schemas used as contracts often need OpenAPI metadata for stable docs and generated clients.",
|
|
8
|
+
"fix": "Add .openapi(...) metadata or .describe(...) fields, especially for public request/response schemas.",
|
|
9
|
+
"suppress_if_nearby": [".openapi(", ".describe("]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "zod-any-unknown-contract",
|
|
13
|
+
"severity": "high",
|
|
14
|
+
"pattern": "z\\.(any|unknown)\\s*\\(",
|
|
15
|
+
"why": "z.any()/z.unknown() weakens generated OpenAPI schemas and client types.",
|
|
16
|
+
"fix": "Replace with explicit object/union/primitive schemas or document a deliberate escape hatch."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "zod-date-serialization-review",
|
|
20
|
+
"severity": "medium",
|
|
21
|
+
"pattern": "z\\.date\\s*\\(",
|
|
22
|
+
"why": "z.date() is a runtime Date object while OpenAPI transports usually serialize dates as strings.",
|
|
23
|
+
"fix": "Use z.string().datetime()/date() for transport contracts, or document how the OpenAPI generator serializes Date values."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "record-additional-properties-review",
|
|
27
|
+
"severity": "medium",
|
|
28
|
+
"pattern": "z\\.record\\s*\\(",
|
|
29
|
+
"why": "Map-like records can generate broad additionalProperties schemas that clients may handle inconsistently.",
|
|
30
|
+
"fix": "Review generated additionalProperties output and add examples/constraints where possible."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "union-discriminator-review",
|
|
34
|
+
"severity": "medium",
|
|
35
|
+
"pattern": "z\\.union\\s*\\(",
|
|
36
|
+
"why": "Plain unions can produce ambiguous anyOf/oneOf output for generated clients.",
|
|
37
|
+
"fix": "Prefer discriminatedUnion when possible or add clear OpenAPI discriminator/description metadata.",
|
|
38
|
+
"suppress_if_nearby": ["discriminatedUnion", "discriminator"]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "runtime-only-zod-effect",
|
|
42
|
+
"severity": "medium",
|
|
43
|
+
"pattern": "\\.(transform|refine|superRefine)\\s*\\(",
|
|
44
|
+
"why": "Runtime-only Zod effects may not be faithfully represented in OpenAPI output.",
|
|
45
|
+
"fix": "Keep transport contracts structural, and document runtime-only validation separately."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "auth-route-missing-401-403-docs",
|
|
49
|
+
"severity": "high",
|
|
50
|
+
"pattern": "(requireAuth|withAuth|authMiddleware|bearerAuth|security)",
|
|
51
|
+
"why": "Authenticated routes should document 401/403 responses so clients can handle auth failures consistently.",
|
|
52
|
+
"fix": "Add explicit 401 and/or 403 response docs with a shared error schema.",
|
|
53
|
+
"suppress_if_nearby": ["401", "403"]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "error-response-without-schema",
|
|
57
|
+
"severity": "medium",
|
|
58
|
+
"pattern": "status\\s*[:=]\\s*(400|401|403|404|409|422|500)|\\b(400|401|403|404|409|422|500)\\s*:",
|
|
59
|
+
"why": "Error responses without a documented schema cause inconsistent client error handling.",
|
|
60
|
+
"fix": "Reference a shared ErrorResponse schema/envelope in OpenAPI responses.",
|
|
61
|
+
"suppress_if_nearby": ["ErrorSchema", "ErrorResponse", "errorSchema"]
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zod-openapi-contract-lint-kit
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Local read-only scanner for Zod/OpenAPI contract drift risks.
|
|
5
|
+
Author: Zod OpenAPI Contract Lint Kit contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vasiliy0/zod-openapi-contract-lint-kit
|
|
8
|
+
Project-URL: Repository, https://github.com/vasiliy0/zod-openapi-contract-lint-kit
|
|
9
|
+
Project-URL: Issues, https://github.com/vasiliy0/zod-openapi-contract-lint-kit/issues
|
|
10
|
+
Keywords: zod,openapi,typescript,api,lint
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# Zod OpenAPI Contract Lint Kit
|
|
26
|
+
|
|
27
|
+
Local read-only prototype for finding common drift risks between Zod schemas, OpenAPI metadata, route responses, and generated API clients.
|
|
28
|
+
|
|
29
|
+
## Why this exists
|
|
30
|
+
|
|
31
|
+
Teams often start with Zod as runtime validation and later generate OpenAPI specs/clients. Drift appears when schemas use constructs generators cannot represent cleanly, when route metadata is missing, or when auth/error responses are inconsistent.
|
|
32
|
+
|
|
33
|
+
This prototype scans TypeScript files and reports review items. It does not execute application code, import project modules, call network APIs, or modify files.
|
|
34
|
+
|
|
35
|
+
## Current v1 checks
|
|
36
|
+
|
|
37
|
+
- Zod object schemas exported without nearby OpenAPI metadata (`.openapi(...)` or `.describe(...)`).
|
|
38
|
+
- `z.any()` / `z.unknown()` in public contracts.
|
|
39
|
+
- `z.date()` fields that may serialize differently in OpenAPI/client transports.
|
|
40
|
+
- `z.record(...)` map types that may need `additionalProperties` review.
|
|
41
|
+
- `z.union(...)` without a nearby discriminator hint.
|
|
42
|
+
- `z.transform(...)`, `.refine(...)`, `.superRefine(...)` in public schemas.
|
|
43
|
+
- Route handlers with auth hints but no visible `401` / `403` response docs nearby.
|
|
44
|
+
- Error responses without an obvious schema/envelope reference.
|
|
45
|
+
|
|
46
|
+
## Try locally
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python3 scanner.py examples
|
|
50
|
+
python3 scanner.py examples --format json
|
|
51
|
+
python3 scanner.py examples --output report.md
|
|
52
|
+
python3 scanner.py examples --fail-on-severity high
|
|
53
|
+
python3 scanner.py examples --min-severity high
|
|
54
|
+
python3 scanner.py examples --only-rule zod-date-serialization-review
|
|
55
|
+
python3 scanner.py --list-rules
|
|
56
|
+
python3 scanner.py --list-rules --format json
|
|
57
|
+
python3 scanner.py examples --ignore-rule runtime-only-zod-effect
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## CI usage
|
|
61
|
+
|
|
62
|
+
See [`docs/CI_USAGE.md`](docs/CI_USAGE.md) for report-only, high-risk-only, and scoped rollout examples.
|
|
63
|
+
|
|
64
|
+
## Safety
|
|
65
|
+
|
|
66
|
+
- Read-only local scan.
|
|
67
|
+
- No GitHub/npm/OpenAPI service account required.
|
|
68
|
+
- No dependency installation required for the prototype.
|
|
69
|
+
- Findings are review prompts, not automatic migration claims.
|
|
70
|
+
- CI failure is opt-in via `--fail-on-severity`.
|
|
71
|
+
- Rule filtering is explicit and local; unknown rule ids fail fast instead of silently changing coverage.
|
|
72
|
+
- `--min-severity` supports high-risk-only reports for CI summaries.
|
|
73
|
+
- `--list-rules` can be used to review active rule coverage before adding the scanner to CI.
|
|
74
|
+
|
|
75
|
+
## Roadmap
|
|
76
|
+
|
|
77
|
+
- Expand fixtures across common Zod/OpenAPI routing patterns.
|
|
78
|
+
- Add fixtures for hono/zod-openapi/express-zod-api route variants.
|
|
79
|
+
- Package as a small developer CLI after the prototype stabilizes.
|
|
80
|
+
- Add more routing-library examples and release notes.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/zod_openapi_contract_lint_kit/__init__.py
|
|
5
|
+
src/zod_openapi_contract_lint_kit/cli.py
|
|
6
|
+
src/zod_openapi_contract_lint_kit/rules.json
|
|
7
|
+
src/zod_openapi_contract_lint_kit.egg-info/PKG-INFO
|
|
8
|
+
src/zod_openapi_contract_lint_kit.egg-info/SOURCES.txt
|
|
9
|
+
src/zod_openapi_contract_lint_kit.egg-info/dependency_links.txt
|
|
10
|
+
src/zod_openapi_contract_lint_kit.egg-info/entry_points.txt
|
|
11
|
+
src/zod_openapi_contract_lint_kit.egg-info/top_level.txt
|
|
12
|
+
tests/test_scanner.py
|
zod_openapi_contract_lint_kit-0.1.1/src/zod_openapi_contract_lint_kit.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
zod_openapi_contract_lint_kit
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import importlib.util
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
from unittest import mock
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
10
|
+
spec = importlib.util.spec_from_file_location("scanner", ROOT / "scanner.py")
|
|
11
|
+
scanner = importlib.util.module_from_spec(spec)
|
|
12
|
+
sys.modules["scanner"] = scanner
|
|
13
|
+
spec.loader.exec_module(scanner)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestScanner(unittest.TestCase):
|
|
17
|
+
def test_example_flags_contract_drift_risks(self):
|
|
18
|
+
report = scanner.scan(ROOT / "examples")
|
|
19
|
+
ids = {finding["rule_id"] for finding in report["findings"]}
|
|
20
|
+
self.assertIn("missing-openapi-metadata", ids)
|
|
21
|
+
self.assertIn("zod-any-unknown-contract", ids)
|
|
22
|
+
self.assertIn("zod-date-serialization-review", ids)
|
|
23
|
+
self.assertIn("record-additional-properties-review", ids)
|
|
24
|
+
self.assertIn("union-discriminator-review", ids)
|
|
25
|
+
self.assertIn("runtime-only-zod-effect", ids)
|
|
26
|
+
self.assertIn("auth-route-missing-401-403-docs", ids)
|
|
27
|
+
self.assertIn("error-response-without-schema", ids)
|
|
28
|
+
self.assertEqual(report["summary_by_rule"]["zod-date-serialization-review"], 1)
|
|
29
|
+
|
|
30
|
+
def test_nearby_openapi_metadata_suppresses_missing_metadata(self):
|
|
31
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
32
|
+
path = Path(tmpdir) / "schema.ts"
|
|
33
|
+
path.write_text('import { z } from "zod";\nexport const UserSchema = z.object({ id: z.string() }).openapi("User");\n', encoding="utf-8")
|
|
34
|
+
report = scanner.scan(path)
|
|
35
|
+
ids = {finding["rule_id"] for finding in report["findings"]}
|
|
36
|
+
self.assertNotIn("missing-openapi-metadata", ids)
|
|
37
|
+
|
|
38
|
+
def test_markdown_contains_safety_note_and_rule_summary(self):
|
|
39
|
+
text = scanner.render_markdown(scanner.scan(ROOT / "examples"))
|
|
40
|
+
self.assertIn("Zod OpenAPI Contract Lint Report", text)
|
|
41
|
+
self.assertIn("Read-only static scan", text)
|
|
42
|
+
self.assertIn("Summary by rule", text)
|
|
43
|
+
self.assertIn("Active rules", text)
|
|
44
|
+
|
|
45
|
+
def test_output_json_and_fail_on_severity(self):
|
|
46
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
47
|
+
output = Path(tmpdir) / "report.json"
|
|
48
|
+
exit_code = scanner.main([str(ROOT / "examples"), "--format", "json", "--output", str(output), "--fail-on-severity", "high"])
|
|
49
|
+
self.assertEqual(exit_code, 1)
|
|
50
|
+
report = json.loads(output.read_text())
|
|
51
|
+
self.assertGreaterEqual(report["summary_by_severity"]["high"], 1)
|
|
52
|
+
self.assertIn("summary_by_rule", report)
|
|
53
|
+
|
|
54
|
+
def test_fail_on_severity_stays_zero_above_medium(self):
|
|
55
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
56
|
+
path = Path(tmpdir) / "schema.ts"
|
|
57
|
+
path.write_text('import { z } from "zod";\nconst StartedAt = z.date();\n', encoding="utf-8")
|
|
58
|
+
self.assertEqual(scanner.main([str(path), "--fail-on-severity", "high"]), 0)
|
|
59
|
+
self.assertEqual(scanner.main([str(path), "--fail-on-severity", "medium"]), 1)
|
|
60
|
+
|
|
61
|
+
def test_only_rule_and_ignore_rule_filter_active_rules(self):
|
|
62
|
+
only_report = scanner.scan(ROOT / "examples", only_rule={"zod-date-serialization-review"})
|
|
63
|
+
self.assertEqual(only_report["active_rule_count"], 1)
|
|
64
|
+
self.assertEqual([f["rule_id"] for f in only_report["findings"]], ["zod-date-serialization-review"])
|
|
65
|
+
|
|
66
|
+
ignored_report = scanner.scan(ROOT / "examples", ignore_rule={"missing-openapi-metadata", "zod-any-unknown-contract", "auth-route-missing-401-403-docs"})
|
|
67
|
+
ids = {finding["rule_id"] for finding in ignored_report["findings"]}
|
|
68
|
+
self.assertNotIn("missing-openapi-metadata", ids)
|
|
69
|
+
self.assertNotIn("zod-any-unknown-contract", ids)
|
|
70
|
+
self.assertNotIn("auth-route-missing-401-403-docs", ids)
|
|
71
|
+
self.assertEqual(ignored_report["summary_by_severity"].get("high"), None)
|
|
72
|
+
|
|
73
|
+
def test_unknown_rule_is_cli_error(self):
|
|
74
|
+
with mock.patch("sys.stderr"):
|
|
75
|
+
with self.assertRaises(SystemExit):
|
|
76
|
+
scanner.main([str(ROOT / "examples"), "--only-rule", "missing-rule"])
|
|
77
|
+
|
|
78
|
+
def test_min_severity_filters_report_output(self):
|
|
79
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
80
|
+
output = Path(tmpdir) / "report.json"
|
|
81
|
+
exit_code = scanner.main([str(ROOT / "examples"), "--format", "json", "--output", str(output), "--min-severity", "high"])
|
|
82
|
+
self.assertEqual(exit_code, 0)
|
|
83
|
+
report = json.loads(output.read_text())
|
|
84
|
+
self.assertEqual(set(report["summary_by_severity"]), {"high"})
|
|
85
|
+
self.assertNotIn("zod-date-serialization-review", report["summary_by_rule"])
|
|
86
|
+
self.assertIn("Filtered findings below high severity.", report["notes"])
|
|
87
|
+
|
|
88
|
+
def test_list_rules_outputs_active_inventory(self):
|
|
89
|
+
rules = scanner.filter_rules(scanner.load_rules(), only_rule={"zod-date-serialization-review"})
|
|
90
|
+
text = scanner.render_rule_inventory(rules)
|
|
91
|
+
self.assertIn("zod-date-serialization-review", text)
|
|
92
|
+
self.assertIn("Severity", text)
|
|
93
|
+
|
|
94
|
+
data = json.loads(scanner.render_rule_inventory(rules, "json"))
|
|
95
|
+
self.assertEqual(data["rules"][0]["id"], "zod-date-serialization-review")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
unittest.main()
|