jentic-openapi-validator 1.0.0a7__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.
- jentic/apitools/openapi/validator/backends/base.py +34 -0
- jentic/apitools/openapi/validator/backends/default/__init__.py +204 -0
- jentic/apitools/openapi/validator/backends/default/rules/__init__.py +182 -0
- jentic/apitools/openapi/validator/backends/default/rules/security.py +200 -0
- jentic/apitools/openapi/validator/backends/default/rules/server.py +136 -0
- jentic/apitools/openapi/validator/backends/default/rules/structural.py +109 -0
- jentic/apitools/openapi/validator/backends/openapi_spec.py +107 -0
- jentic/apitools/openapi/validator/backends/py.typed +0 -0
- jentic/apitools/openapi/validator/core/__init__.py +5 -0
- jentic/apitools/openapi/validator/core/diagnostics.py +99 -0
- jentic/apitools/openapi/validator/core/openapi_validator.py +191 -0
- jentic/apitools/openapi/validator/core/py.typed +0 -0
- jentic_openapi_validator-1.0.0a7.dist-info/METADATA +224 -0
- jentic_openapi_validator-1.0.0a7.dist-info/RECORD +18 -0
- jentic_openapi_validator-1.0.0a7.dist-info/WHEEL +4 -0
- jentic_openapi_validator-1.0.0a7.dist-info/entry_points.txt +5 -0
- jentic_openapi_validator-1.0.0a7.dist-info/licenses/LICENSE +202 -0
- jentic_openapi_validator-1.0.0a7.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from jentic.apitools.openapi.validator.core.diagnostics import ValidationResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = ["BaseValidatorBackend"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseValidatorBackend(ABC):
|
|
16
|
+
"""Interface that all Validator backends must implement."""
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def validate(
|
|
20
|
+
self, document: str | dict, *, base_url: str | None = None, target: str | None = None
|
|
21
|
+
) -> ValidationResult:
|
|
22
|
+
"""Validate an OpenAPI document given by URI or file path or text.
|
|
23
|
+
Returns a ValidationResult (could be a list of errors, or an object)."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def accepts() -> Sequence[str]:
|
|
29
|
+
"""Return the document formats this backend can accept.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Sequence of format identifiers (e.g., "uri", "text", "dict")
|
|
33
|
+
"""
|
|
34
|
+
...
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Default OpenAPI validator backend with rule-based validation system.
|
|
3
|
+
|
|
4
|
+
This backend provides a collection of validation rules for common OpenAPI
|
|
5
|
+
specification issues including structural validation, server validation,
|
|
6
|
+
and security validation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from lsprotocol import types as lsp
|
|
12
|
+
|
|
13
|
+
from jentic.apitools.openapi.parser.core import OpenAPIParser
|
|
14
|
+
from jentic.apitools.openapi.parser.core.exceptions import DocumentLoadError, DocumentParseError
|
|
15
|
+
|
|
16
|
+
from ...core.diagnostics import JenticDiagnostic, ValidationResult
|
|
17
|
+
from ..base import BaseValidatorBackend
|
|
18
|
+
from .rules import RuleRegistry
|
|
19
|
+
from .rules.security import SecuritySchemeReferenceRule, UnusedSecuritySchemeRule
|
|
20
|
+
from .rules.server import ServerUrlRule
|
|
21
|
+
from .rules.structural import InfoObjectRule, PathsRule
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ["DefaultOpenAPIValidatorBackend"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DefaultOpenAPIValidatorBackend(BaseValidatorBackend):
|
|
28
|
+
"""
|
|
29
|
+
Default OpenAPI validator backend using a rule-based validation system.
|
|
30
|
+
|
|
31
|
+
This validator applies a set of predefined rules to check for common
|
|
32
|
+
issues in OpenAPI specifications. Rules cover:
|
|
33
|
+
- Structural validation (info, paths)
|
|
34
|
+
- Server validation (servers array, URLs)
|
|
35
|
+
- Security validation (scheme references, unused schemes)
|
|
36
|
+
|
|
37
|
+
The validator can be customized by providing a custom RuleRegistry
|
|
38
|
+
with different rules.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self, rule_registry: RuleRegistry | None = None, parser: OpenAPIParser | None = None
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Initialize the default OpenAPI validator.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
rule_registry: Optional custom rule registry. If None, uses default rules.
|
|
49
|
+
parser: Optional OpenAPIParser instance. If None, creates a default parser.
|
|
50
|
+
"""
|
|
51
|
+
if rule_registry is None:
|
|
52
|
+
rule_registry = self._create_default_registry()
|
|
53
|
+
self.registry = rule_registry
|
|
54
|
+
self.parser = parser if parser else OpenAPIParser()
|
|
55
|
+
|
|
56
|
+
def validate(
|
|
57
|
+
self, document: str | dict, *, base_url: str | None = None, target: str | None = None
|
|
58
|
+
) -> ValidationResult:
|
|
59
|
+
"""
|
|
60
|
+
Validate an OpenAPI document using the registered rules.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
document: Path to the OpenAPI document file to validate, or dict containing the document
|
|
64
|
+
base_url: Optional base URL for resolving references
|
|
65
|
+
target: Optional target identifier for validation context
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
ValidationResult containing diagnostics for all rule violations
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
TypeError: If document type is not supported
|
|
72
|
+
"""
|
|
73
|
+
if isinstance(document, str):
|
|
74
|
+
return self._validate_uri(document, base_url=base_url, target=target)
|
|
75
|
+
elif isinstance(document, dict):
|
|
76
|
+
return self._validate_dict(document, base_url=base_url, target=target)
|
|
77
|
+
else:
|
|
78
|
+
raise TypeError(f"Unsupported document type: {type(document)!r}")
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def accepts() -> list[Literal["uri", "dict"]]:
|
|
82
|
+
"""
|
|
83
|
+
Return the document formats this validator accepts.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Sequence of supported document format identifiers:
|
|
87
|
+
- "uri": File path or URI pointing to OpenAPI Document
|
|
88
|
+
- "dict": Python dictionary containing OpenAPI Document data
|
|
89
|
+
"""
|
|
90
|
+
return ["uri", "dict"]
|
|
91
|
+
|
|
92
|
+
def _validate_uri(
|
|
93
|
+
self, document: str, *, base_url: str | None = None, target: str | None = None
|
|
94
|
+
) -> ValidationResult:
|
|
95
|
+
"""
|
|
96
|
+
Validate an OpenAPI document from a URI or file path.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
document: Path to the OpenAPI document file or URI
|
|
100
|
+
base_url: Optional base URL for resolving references
|
|
101
|
+
target: Optional target identifier for validation context
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
ValidationResult containing diagnostics for all rule violations
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
# Check if parser backend supports URI directly
|
|
108
|
+
if "uri" in self.parser.backend.accepts():
|
|
109
|
+
# Let parser handle URI loading
|
|
110
|
+
document_dict = self.parser.parse(document)
|
|
111
|
+
else:
|
|
112
|
+
# Manually load URI and parse the text
|
|
113
|
+
document_text = self.parser.load_uri(document)
|
|
114
|
+
document_dict = self.parser.parse(document_text)
|
|
115
|
+
|
|
116
|
+
# Validate the parsed document
|
|
117
|
+
return self._validate_dict(document_dict, base_url=base_url, target=target)
|
|
118
|
+
|
|
119
|
+
except (DocumentParseError, DocumentLoadError) as e:
|
|
120
|
+
# Handle document parsing/loading errors
|
|
121
|
+
diagnostic = JenticDiagnostic(
|
|
122
|
+
range=lsp.Range(
|
|
123
|
+
start=lsp.Position(line=0, character=0),
|
|
124
|
+
end=lsp.Position(line=0, character=12),
|
|
125
|
+
),
|
|
126
|
+
severity=lsp.DiagnosticSeverity.Error,
|
|
127
|
+
code="document-parse-error",
|
|
128
|
+
source="default-validator",
|
|
129
|
+
message=f"Failed to parse document: {str(e)}",
|
|
130
|
+
)
|
|
131
|
+
diagnostic.set_target(target)
|
|
132
|
+
return ValidationResult(diagnostics=[diagnostic])
|
|
133
|
+
except Exception as e:
|
|
134
|
+
# Handle any other unexpected errors
|
|
135
|
+
diagnostic = JenticDiagnostic(
|
|
136
|
+
range=lsp.Range(
|
|
137
|
+
start=lsp.Position(line=0, character=0),
|
|
138
|
+
end=lsp.Position(line=0, character=12),
|
|
139
|
+
),
|
|
140
|
+
severity=lsp.DiagnosticSeverity.Error,
|
|
141
|
+
code="default-validator-error",
|
|
142
|
+
source="default-validator",
|
|
143
|
+
message=f"Unexpected error: {str(e)}",
|
|
144
|
+
)
|
|
145
|
+
diagnostic.set_target(target)
|
|
146
|
+
return ValidationResult(diagnostics=[diagnostic])
|
|
147
|
+
|
|
148
|
+
def _validate_dict(
|
|
149
|
+
self, document: dict, *, base_url: str | None = None, target: str | None = None
|
|
150
|
+
) -> ValidationResult:
|
|
151
|
+
"""
|
|
152
|
+
Validate an OpenAPI document from a dictionary.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
document: The OpenAPI document as a dictionary
|
|
156
|
+
base_url: Optional base URL for resolving references (not used)
|
|
157
|
+
target: Optional target identifier for validation context
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
ValidationResult containing diagnostics for all rule violations
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
# Run all rules through the registry
|
|
164
|
+
diagnostics = self.registry.validate(document, target=target)
|
|
165
|
+
return ValidationResult(diagnostics=diagnostics)
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
# Catch any unexpected errors during validation
|
|
169
|
+
msg = str(e)
|
|
170
|
+
diagnostic = JenticDiagnostic(
|
|
171
|
+
range=lsp.Range(
|
|
172
|
+
start=lsp.Position(line=0, character=0),
|
|
173
|
+
end=lsp.Position(line=0, character=12),
|
|
174
|
+
),
|
|
175
|
+
severity=lsp.DiagnosticSeverity.Error,
|
|
176
|
+
code="default-validator-error",
|
|
177
|
+
source="default-validator",
|
|
178
|
+
message=msg,
|
|
179
|
+
)
|
|
180
|
+
diagnostic.set_target(target)
|
|
181
|
+
return ValidationResult(diagnostics=[diagnostic])
|
|
182
|
+
|
|
183
|
+
@staticmethod
|
|
184
|
+
def _create_default_registry() -> RuleRegistry:
|
|
185
|
+
"""
|
|
186
|
+
Create the default rule registry with all standard rules.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
A RuleRegistry with all default validation rules registered
|
|
190
|
+
"""
|
|
191
|
+
registry = RuleRegistry(source="default-validator")
|
|
192
|
+
|
|
193
|
+
# Register structural rules
|
|
194
|
+
registry.register(InfoObjectRule())
|
|
195
|
+
registry.register(PathsRule())
|
|
196
|
+
|
|
197
|
+
# Register server rules (only validate format if servers exist)
|
|
198
|
+
registry.register(ServerUrlRule())
|
|
199
|
+
|
|
200
|
+
# Register security rules
|
|
201
|
+
registry.register(SecuritySchemeReferenceRule())
|
|
202
|
+
registry.register(UnusedSecuritySchemeRule())
|
|
203
|
+
|
|
204
|
+
return registry
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation rules system for the default OpenAPI validator backend.
|
|
3
|
+
|
|
4
|
+
This module provides the infrastructure for defining and executing validation rules
|
|
5
|
+
on OpenAPI specifications. Rules are modular, testable, and composable.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from lsprotocol import types as lsp
|
|
13
|
+
|
|
14
|
+
from ....core.diagnostics import JenticDiagnostic
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = ["BaseRule", "RuleRegistry", "ValidationIssue"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ValidationIssue:
|
|
22
|
+
"""
|
|
23
|
+
Represents a validation issue found by a rule.
|
|
24
|
+
|
|
25
|
+
This is a lightweight data structure that captures the essential information
|
|
26
|
+
about a validation problem. It will be converted to a JenticDiagnostic
|
|
27
|
+
by the rule registry.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
code: Error code (e.g., "MISSING_SERVER_URL")
|
|
31
|
+
message: Human-readable error message
|
|
32
|
+
severity: Diagnostic severity level
|
|
33
|
+
path: JSON path to the problematic element (e.g., ["servers", 0, "url"])
|
|
34
|
+
fixable: Whether this issue can be automatically fixed
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
code: str
|
|
38
|
+
message: str
|
|
39
|
+
severity: lsp.DiagnosticSeverity = lsp.DiagnosticSeverity.Error
|
|
40
|
+
path: list[str | int] = field(default_factory=list)
|
|
41
|
+
fixable: bool = True
|
|
42
|
+
|
|
43
|
+
def to_diagnostic(self, source: str, target: str | None = None) -> JenticDiagnostic:
|
|
44
|
+
"""
|
|
45
|
+
Convert this ValidationIssue to a JenticDiagnostic.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
source: Source identifier for the diagnostic (e.g., "default-validator")
|
|
49
|
+
target: Optional target identifier for validation context
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
A JenticDiagnostic instance ready to be returned to the user
|
|
53
|
+
"""
|
|
54
|
+
diagnostic = JenticDiagnostic(
|
|
55
|
+
range=lsp.Range(
|
|
56
|
+
start=lsp.Position(line=0, character=0),
|
|
57
|
+
end=lsp.Position(line=0, character=0),
|
|
58
|
+
),
|
|
59
|
+
severity=self.severity,
|
|
60
|
+
code=self.code,
|
|
61
|
+
source=source,
|
|
62
|
+
message=self.message,
|
|
63
|
+
)
|
|
64
|
+
diagnostic.set_path(self.path)
|
|
65
|
+
diagnostic.set_target(target)
|
|
66
|
+
diagnostic.set_fixable(self.fixable)
|
|
67
|
+
return diagnostic
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BaseRule(ABC):
|
|
71
|
+
"""
|
|
72
|
+
Abstract base class for validation rules.
|
|
73
|
+
|
|
74
|
+
Each rule should focus on validating a specific aspect of the OpenAPI specification.
|
|
75
|
+
Rules are designed to be stateless and reusable.
|
|
76
|
+
|
|
77
|
+
Subclasses must implement the `validate` method which examines the spec
|
|
78
|
+
and returns a list of ValidationIssue objects for any problems found.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def rule_id(self) -> str:
|
|
84
|
+
"""
|
|
85
|
+
Return a machine-readable identifier for this rule.
|
|
86
|
+
|
|
87
|
+
The rule ID should be in dash-case (kebab-case) format for consistency
|
|
88
|
+
with other validation tools like Redocly.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Rule identifier (e.g., "server-url-validation")
|
|
92
|
+
"""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def name(self) -> str:
|
|
98
|
+
"""
|
|
99
|
+
Return a human-readable name for this rule.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Rule name (e.g., "Server URL Validation")
|
|
103
|
+
"""
|
|
104
|
+
...
|
|
105
|
+
|
|
106
|
+
@abstractmethod
|
|
107
|
+
def validate(self, spec_data: dict[str, Any]) -> list[ValidationIssue]:
|
|
108
|
+
"""
|
|
109
|
+
Validate the OpenAPI specification.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
spec_data: The parsed OpenAPI specification as a dictionary
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
List of ValidationIssue objects for any problems found.
|
|
116
|
+
Empty list if no issues.
|
|
117
|
+
"""
|
|
118
|
+
...
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class RuleRegistry:
|
|
122
|
+
"""
|
|
123
|
+
Registry for managing and executing validation rules.
|
|
124
|
+
|
|
125
|
+
The registry maintains a collection of validation rules and provides
|
|
126
|
+
methods to execute them against OpenAPI specifications.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, source: str = "default-validator"):
|
|
130
|
+
"""
|
|
131
|
+
Initialize the rule registry.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
source: Source identifier for diagnostics (default: "default-validator")
|
|
135
|
+
"""
|
|
136
|
+
self.source = source
|
|
137
|
+
self.rules: list[BaseRule] = []
|
|
138
|
+
|
|
139
|
+
def register(self, rule: BaseRule) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Register a validation rule.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
rule: The rule to register
|
|
145
|
+
"""
|
|
146
|
+
self.rules.append(rule)
|
|
147
|
+
|
|
148
|
+
def register_all(self, rules: list[BaseRule]) -> None:
|
|
149
|
+
"""
|
|
150
|
+
Register multiple validation rules at once.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
rules: List of rules to register
|
|
154
|
+
"""
|
|
155
|
+
self.rules.extend(rules)
|
|
156
|
+
|
|
157
|
+
def validate(
|
|
158
|
+
self, spec_data: dict[str, Any], target: str | None = None
|
|
159
|
+
) -> list[JenticDiagnostic]:
|
|
160
|
+
"""
|
|
161
|
+
Run all registered rules against the specification.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
spec_data: The parsed OpenAPI specification as a dictionary
|
|
165
|
+
target: Optional target identifier for validation context
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
List of JenticDiagnostic objects for all issues found across all rules
|
|
169
|
+
"""
|
|
170
|
+
diagnostics: list[JenticDiagnostic] = []
|
|
171
|
+
|
|
172
|
+
for rule in self.rules:
|
|
173
|
+
issues = rule.validate(spec_data)
|
|
174
|
+
for issue in issues:
|
|
175
|
+
diagnostic = issue.to_diagnostic(source=self.source, target=target)
|
|
176
|
+
diagnostics.append(diagnostic)
|
|
177
|
+
|
|
178
|
+
return diagnostics
|
|
179
|
+
|
|
180
|
+
def clear(self) -> None:
|
|
181
|
+
"""Clear all registered rules."""
|
|
182
|
+
self.rules.clear()
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Security validation rules for OpenAPI specifications.
|
|
3
|
+
|
|
4
|
+
These rules validate security schemes and their usage throughout the specification.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from lsprotocol import types as lsp
|
|
10
|
+
|
|
11
|
+
from . import BaseRule, ValidationIssue
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = ["SecuritySchemeReferenceRule", "UnusedSecuritySchemeRule"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# HTTP methods defined in OpenAPI 3.x specification
|
|
18
|
+
_HTTP_METHODS = {"get", "put", "post", "delete", "options", "head", "patch", "trace"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SecuritySchemeReferenceRule(BaseRule):
|
|
22
|
+
"""
|
|
23
|
+
Validates that all security scheme references point to defined schemes.
|
|
24
|
+
|
|
25
|
+
Checks both global security requirements and operation-level security requirements
|
|
26
|
+
to ensure they only reference schemes defined in components.securitySchemes.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def rule_id(self) -> str:
|
|
31
|
+
return "security-scheme-reference"
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def name(self) -> str:
|
|
35
|
+
return "Security Scheme Reference Validation"
|
|
36
|
+
|
|
37
|
+
def validate(self, spec_data: dict[str, Any]) -> list[ValidationIssue]:
|
|
38
|
+
issues: list[ValidationIssue] = []
|
|
39
|
+
|
|
40
|
+
# Get defined security schemes
|
|
41
|
+
components = spec_data.get("components", {})
|
|
42
|
+
security_schemes = components.get("securitySchemes", {})
|
|
43
|
+
defined_schemes: set[str] = (
|
|
44
|
+
set(security_schemes.keys()) if isinstance(security_schemes, dict) else set()
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Check global security requirements
|
|
48
|
+
issues.extend(self._check_global_security(spec_data, defined_schemes))
|
|
49
|
+
|
|
50
|
+
# Check operation-level security requirements
|
|
51
|
+
issues.extend(self._check_operation_security(spec_data, defined_schemes))
|
|
52
|
+
|
|
53
|
+
return issues
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _check_global_security(
|
|
57
|
+
spec_data: dict[str, Any], defined_schemes: set[str]
|
|
58
|
+
) -> list[ValidationIssue]:
|
|
59
|
+
"""Check global security requirements."""
|
|
60
|
+
issues: list[ValidationIssue] = []
|
|
61
|
+
global_security = spec_data.get("security", [])
|
|
62
|
+
|
|
63
|
+
if not isinstance(global_security, list):
|
|
64
|
+
return issues
|
|
65
|
+
|
|
66
|
+
for sec_req in global_security:
|
|
67
|
+
if not isinstance(sec_req, dict):
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
for scheme in sec_req.keys():
|
|
71
|
+
if scheme not in defined_schemes:
|
|
72
|
+
issues.append(
|
|
73
|
+
ValidationIssue(
|
|
74
|
+
code="UNDEFINED_SECURITY_SCHEME_REFERENCE",
|
|
75
|
+
message=f"Global security requirement references undefined scheme '{scheme}'.",
|
|
76
|
+
severity=lsp.DiagnosticSeverity.Error,
|
|
77
|
+
path=["security"],
|
|
78
|
+
fixable=False,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return issues
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def _check_operation_security(
|
|
86
|
+
spec_data: dict[str, Any], defined_schemes: set[str]
|
|
87
|
+
) -> list[ValidationIssue]:
|
|
88
|
+
"""Check operation-level security requirements."""
|
|
89
|
+
issues: list[ValidationIssue] = []
|
|
90
|
+
paths = spec_data.get("paths", {})
|
|
91
|
+
|
|
92
|
+
if not isinstance(paths, dict):
|
|
93
|
+
return issues
|
|
94
|
+
|
|
95
|
+
for path_str, path_item in paths.items():
|
|
96
|
+
if not isinstance(path_item, dict):
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
for method, operation in path_item.items():
|
|
100
|
+
if method not in _HTTP_METHODS:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if not isinstance(operation, dict):
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
op_security = operation.get("security", [])
|
|
107
|
+
if not isinstance(op_security, list):
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
for sec_req in op_security:
|
|
111
|
+
if not isinstance(sec_req, dict):
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
for scheme in sec_req.keys():
|
|
115
|
+
if scheme not in defined_schemes:
|
|
116
|
+
issues.append(
|
|
117
|
+
ValidationIssue(
|
|
118
|
+
code="UNDEFINED_SECURITY_SCHEME_REFERENCE",
|
|
119
|
+
message=f"Operation '{method.upper()}' at path '{path_str}' references undefined scheme '{scheme}'.",
|
|
120
|
+
severity=lsp.DiagnosticSeverity.Error,
|
|
121
|
+
path=[
|
|
122
|
+
"paths",
|
|
123
|
+
path_str,
|
|
124
|
+
method,
|
|
125
|
+
"security",
|
|
126
|
+
],
|
|
127
|
+
fixable=False,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return issues
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class UnusedSecuritySchemeRule(BaseRule):
|
|
135
|
+
"""
|
|
136
|
+
Detects security schemes that are defined but never used.
|
|
137
|
+
|
|
138
|
+
This is a warning-level rule that helps identify potentially dead code
|
|
139
|
+
in the security scheme definitions.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def rule_id(self) -> str:
|
|
144
|
+
return "unused-security-scheme"
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def name(self) -> str:
|
|
148
|
+
return "Unused Security Scheme Detection"
|
|
149
|
+
|
|
150
|
+
def validate(self, spec_data: dict[str, Any]) -> list[ValidationIssue]:
|
|
151
|
+
issues: list[ValidationIssue] = []
|
|
152
|
+
|
|
153
|
+
# Get defined security schemes
|
|
154
|
+
components = spec_data.get("components", {})
|
|
155
|
+
security_schemes = components.get("securitySchemes", {})
|
|
156
|
+
defined_schemes: set[str] = (
|
|
157
|
+
set(security_schemes.keys()) if isinstance(security_schemes, dict) else set()
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Collect all referenced schemes
|
|
161
|
+
referenced_schemes: set[str] = set()
|
|
162
|
+
|
|
163
|
+
# Check global security
|
|
164
|
+
global_security = spec_data.get("security", [])
|
|
165
|
+
if isinstance(global_security, list):
|
|
166
|
+
for sec_req in global_security:
|
|
167
|
+
if isinstance(sec_req, dict):
|
|
168
|
+
referenced_schemes.update(sec_req.keys())
|
|
169
|
+
|
|
170
|
+
# Check operation-level security
|
|
171
|
+
paths = spec_data.get("paths", {})
|
|
172
|
+
if isinstance(paths, dict):
|
|
173
|
+
for path_item in paths.values():
|
|
174
|
+
if not isinstance(path_item, dict):
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
for method, operation in path_item.items():
|
|
178
|
+
if method not in _HTTP_METHODS or not isinstance(operation, dict):
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
op_security = operation.get("security", [])
|
|
182
|
+
if isinstance(op_security, list):
|
|
183
|
+
for sec_req in op_security:
|
|
184
|
+
if isinstance(sec_req, dict):
|
|
185
|
+
referenced_schemes.update(sec_req.keys())
|
|
186
|
+
|
|
187
|
+
# Find unused schemes
|
|
188
|
+
unused = defined_schemes - referenced_schemes
|
|
189
|
+
for scheme in unused:
|
|
190
|
+
issues.append(
|
|
191
|
+
ValidationIssue(
|
|
192
|
+
code="UNUSED_SECURITY_SCHEME_DEFINED",
|
|
193
|
+
message=f"Security scheme '{scheme}' is defined in components.securitySchemes but not used in any security requirement.",
|
|
194
|
+
severity=lsp.DiagnosticSeverity.Warning,
|
|
195
|
+
path=["components", "securitySchemes", scheme],
|
|
196
|
+
fixable=True,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return issues
|