delimit-cli 2.3.2 → 3.0.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/.dockerignore +7 -0
- package/.github/workflows/ci.yml +22 -0
- package/CHANGELOG.md +33 -0
- package/CODE_OF_CONDUCT.md +48 -0
- package/CONTRIBUTING.md +67 -0
- package/Dockerfile +9 -0
- package/LICENSE +21 -0
- package/README.md +51 -130
- package/SECURITY.md +42 -0
- package/adapters/codex-forge.js +107 -0
- package/adapters/codex-jamsons.js +142 -0
- package/adapters/codex-security.js +94 -0
- package/adapters/gemini-forge.js +120 -0
- package/adapters/gemini-jamsons.js +152 -0
- package/bin/delimit-cli.js +52 -2
- package/bin/delimit-setup.js +258 -0
- package/gateway/ai/backends/__init__.py +0 -0
- package/gateway/ai/backends/async_utils.py +21 -0
- package/gateway/ai/backends/deploy_bridge.py +150 -0
- package/gateway/ai/backends/gateway_core.py +261 -0
- package/gateway/ai/backends/generate_bridge.py +38 -0
- package/gateway/ai/backends/governance_bridge.py +196 -0
- package/gateway/ai/backends/intel_bridge.py +59 -0
- package/gateway/ai/backends/memory_bridge.py +93 -0
- package/gateway/ai/backends/ops_bridge.py +137 -0
- package/gateway/ai/backends/os_bridge.py +82 -0
- package/gateway/ai/backends/repo_bridge.py +117 -0
- package/gateway/ai/backends/ui_bridge.py +118 -0
- package/gateway/ai/backends/vault_bridge.py +129 -0
- package/gateway/ai/server.py +1182 -0
- package/gateway/core/__init__.py +3 -0
- package/gateway/core/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/auto_baseline.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/ci_formatter.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/contract_ledger.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/dependency_graph.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/dependency_manifest.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/diff_engine_v2.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/event_backbone.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/event_schema.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/explainer.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/gateway.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/gateway_v2.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/gateway_v3.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/impact_analyzer.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/policy_engine.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/registry.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/registry_v2.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/registry_v3.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/semver_classifier.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/spec_detector.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/surface_bridge.cpython-310.pyc +0 -0
- package/gateway/core/auto_baseline.py +304 -0
- package/gateway/core/ci_formatter.py +283 -0
- package/gateway/core/complexity_analyzer.py +386 -0
- package/gateway/core/contract_ledger.py +345 -0
- package/gateway/core/dependency_graph.py +218 -0
- package/gateway/core/dependency_manifest.py +223 -0
- package/gateway/core/diff_engine_v2.py +477 -0
- package/gateway/core/diff_engine_v2.py.bak +426 -0
- package/gateway/core/event_backbone.py +268 -0
- package/gateway/core/event_schema.py +258 -0
- package/gateway/core/explainer.py +438 -0
- package/gateway/core/gateway.py +128 -0
- package/gateway/core/gateway_v2.py +154 -0
- package/gateway/core/gateway_v3.py +224 -0
- package/gateway/core/impact_analyzer.py +163 -0
- package/gateway/core/policies/default.yml +13 -0
- package/gateway/core/policies/relaxed.yml +48 -0
- package/gateway/core/policies/strict.yml +55 -0
- package/gateway/core/policy_engine.py +464 -0
- package/gateway/core/registry.py +52 -0
- package/gateway/core/registry_v2.py +132 -0
- package/gateway/core/registry_v3.py +134 -0
- package/gateway/core/semver_classifier.py +152 -0
- package/gateway/core/spec_detector.py +130 -0
- package/gateway/core/surface_bridge.py +307 -0
- package/gateway/core/zero_spec/__init__.py +4 -0
- package/gateway/core/zero_spec/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/detector.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/express_extractor.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/fastapi_extractor.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/nestjs_extractor.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/detector.py +353 -0
- package/gateway/core/zero_spec/express_extractor.py +483 -0
- package/gateway/core/zero_spec/fastapi_extractor.py +254 -0
- package/gateway/core/zero_spec/nestjs_extractor.py +369 -0
- package/gateway/tasks/__init__.py +1 -0
- package/gateway/tasks/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/check_policy.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/check_policy_v2.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/check_policy_v3.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/explain_diff.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/explain_diff_v2.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/validate_api.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/validate_api_v2.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/validate_api_v3.cpython-310.pyc +0 -0
- package/gateway/tasks/check_policy.py +177 -0
- package/gateway/tasks/check_policy_v2.py +255 -0
- package/gateway/tasks/check_policy_v3.py +255 -0
- package/gateway/tasks/explain_diff.py +305 -0
- package/gateway/tasks/explain_diff_v2.py +267 -0
- package/gateway/tasks/validate_api.py +131 -0
- package/gateway/tasks/validate_api_v2.py +208 -0
- package/gateway/tasks/validate_api_v3.py +163 -0
- package/package.json +3 -3
- package/adapters/codex-skill.js +0 -87
- package/adapters/cursor-extension.js +0 -190
- package/adapters/gemini-action.js +0 -93
- package/adapters/openai-function.js +0 -112
- package/adapters/xai-plugin.js +0 -151
- package/test-decision-engine.js +0 -181
- package/test-hook.js +0 -27
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
from typing import Dict, List, Any
|
|
3
|
+
from core.registry import task_registry
|
|
4
|
+
from schemas.base import TaskRequest
|
|
5
|
+
|
|
6
|
+
register_task = task_registry.register
|
|
7
|
+
|
|
8
|
+
@register_task("check-policy", version="v1", description="Check API against policy rules")
|
|
9
|
+
def check_policy_handler(request: TaskRequest) -> Dict[str, Any]:
|
|
10
|
+
"""Validate API specification against organizational policies"""
|
|
11
|
+
|
|
12
|
+
files = request.files
|
|
13
|
+
if not files:
|
|
14
|
+
raise ValueError("check-policy requires at least one API spec file")
|
|
15
|
+
|
|
16
|
+
# Load policy from config or use defaults
|
|
17
|
+
policy = request.config.get("policy", get_default_policy())
|
|
18
|
+
if isinstance(policy, str):
|
|
19
|
+
# If policy is a file path, load it
|
|
20
|
+
policy = load_policy(policy)
|
|
21
|
+
|
|
22
|
+
violations = []
|
|
23
|
+
warnings = []
|
|
24
|
+
passed_checks = []
|
|
25
|
+
|
|
26
|
+
for file_path in files:
|
|
27
|
+
spec = load_spec(file_path)
|
|
28
|
+
|
|
29
|
+
# Check various policy rules
|
|
30
|
+
violations_found, warnings_found, passed = check_spec_against_policy(spec, policy)
|
|
31
|
+
violations.extend(violations_found)
|
|
32
|
+
warnings.extend(warnings_found)
|
|
33
|
+
passed_checks.extend(passed)
|
|
34
|
+
|
|
35
|
+
compliance_score = calculate_compliance_score(violations, warnings, passed_checks)
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
"compliant": len(violations) == 0,
|
|
39
|
+
"violations": violations,
|
|
40
|
+
"warnings": warnings,
|
|
41
|
+
"passed_checks": passed_checks,
|
|
42
|
+
"compliance_score": compliance_score,
|
|
43
|
+
"summary": {
|
|
44
|
+
"total_violations": len(violations),
|
|
45
|
+
"total_warnings": len(warnings),
|
|
46
|
+
"total_passed": len(passed_checks)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
def load_spec(file_path: str) -> Dict:
|
|
51
|
+
"""Load API specification"""
|
|
52
|
+
with open(file_path, 'r') as f:
|
|
53
|
+
if file_path.endswith('.yaml') or file_path.endswith('.yml'):
|
|
54
|
+
return yaml.safe_load(f)
|
|
55
|
+
else:
|
|
56
|
+
import json
|
|
57
|
+
return json.load(f)
|
|
58
|
+
|
|
59
|
+
def load_policy(policy_path: str) -> Dict:
|
|
60
|
+
"""Load policy rules from file"""
|
|
61
|
+
with open(policy_path, 'r') as f:
|
|
62
|
+
return yaml.safe_load(f)
|
|
63
|
+
|
|
64
|
+
def get_default_policy() -> Dict:
|
|
65
|
+
"""Get default policy rules"""
|
|
66
|
+
return {
|
|
67
|
+
"rules": {
|
|
68
|
+
"require_version": True,
|
|
69
|
+
"require_description": True,
|
|
70
|
+
"require_auth": True,
|
|
71
|
+
"require_https": True,
|
|
72
|
+
"max_path_depth": 5,
|
|
73
|
+
"naming_convention": "kebab-case",
|
|
74
|
+
"require_response_codes": ["200", "400", "500"],
|
|
75
|
+
"require_request_validation": True
|
|
76
|
+
},
|
|
77
|
+
"severity": {
|
|
78
|
+
"require_version": "high",
|
|
79
|
+
"require_auth": "high",
|
|
80
|
+
"require_https": "high",
|
|
81
|
+
"require_description": "low",
|
|
82
|
+
"max_path_depth": "medium",
|
|
83
|
+
"naming_convention": "low"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def check_spec_against_policy(spec: Dict, policy: Dict) -> tuple:
|
|
88
|
+
"""Check specification against policy rules"""
|
|
89
|
+
violations = []
|
|
90
|
+
warnings = []
|
|
91
|
+
passed = []
|
|
92
|
+
|
|
93
|
+
rules = policy.get("rules", {})
|
|
94
|
+
severity = policy.get("severity", {})
|
|
95
|
+
|
|
96
|
+
# Check version requirement
|
|
97
|
+
if rules.get("require_version"):
|
|
98
|
+
if "openapi" in spec or "swagger" in spec:
|
|
99
|
+
passed.append("API version specified")
|
|
100
|
+
else:
|
|
101
|
+
violation = {
|
|
102
|
+
"rule": "require_version",
|
|
103
|
+
"message": "API specification must include version",
|
|
104
|
+
"severity": severity.get("require_version", "medium")
|
|
105
|
+
}
|
|
106
|
+
if violation["severity"] == "high":
|
|
107
|
+
violations.append(violation)
|
|
108
|
+
else:
|
|
109
|
+
warnings.append(violation["message"])
|
|
110
|
+
|
|
111
|
+
# Check description
|
|
112
|
+
if rules.get("require_description"):
|
|
113
|
+
if spec.get("info", {}).get("description"):
|
|
114
|
+
passed.append("API description present")
|
|
115
|
+
else:
|
|
116
|
+
violation = {
|
|
117
|
+
"rule": "require_description",
|
|
118
|
+
"message": "API must have a description",
|
|
119
|
+
"severity": severity.get("require_description", "low")
|
|
120
|
+
}
|
|
121
|
+
warnings.append(violation["message"])
|
|
122
|
+
|
|
123
|
+
# Check security/auth
|
|
124
|
+
if rules.get("require_auth"):
|
|
125
|
+
if spec.get("security") or spec.get("securityDefinitions") or spec.get("components", {}).get("securitySchemes"):
|
|
126
|
+
passed.append("Security definitions present")
|
|
127
|
+
else:
|
|
128
|
+
violations.append({
|
|
129
|
+
"rule": "require_auth",
|
|
130
|
+
"message": "API must define security schemes",
|
|
131
|
+
"severity": severity.get("require_auth", "high")
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
# Check HTTPS requirement
|
|
135
|
+
if rules.get("require_https"):
|
|
136
|
+
servers = spec.get("servers", [])
|
|
137
|
+
if servers:
|
|
138
|
+
non_https = [s for s in servers if not s.get("url", "").startswith("https://")]
|
|
139
|
+
if non_https:
|
|
140
|
+
violations.append({
|
|
141
|
+
"rule": "require_https",
|
|
142
|
+
"message": f"All servers must use HTTPS. Found non-HTTPS: {len(non_https)}",
|
|
143
|
+
"severity": severity.get("require_https", "high")
|
|
144
|
+
})
|
|
145
|
+
else:
|
|
146
|
+
passed.append("All servers use HTTPS")
|
|
147
|
+
else:
|
|
148
|
+
warnings.append("No servers defined to check HTTPS requirement")
|
|
149
|
+
|
|
150
|
+
# Check path depth
|
|
151
|
+
max_depth = rules.get("max_path_depth", 5)
|
|
152
|
+
if "paths" in spec:
|
|
153
|
+
for path in spec["paths"]:
|
|
154
|
+
depth = len([p for p in path.split("/") if p])
|
|
155
|
+
if depth > max_depth:
|
|
156
|
+
warnings.append(f"Path exceeds max depth ({max_depth}): {path}")
|
|
157
|
+
|
|
158
|
+
return violations, warnings, passed
|
|
159
|
+
|
|
160
|
+
def calculate_compliance_score(violations: List, warnings: List, passed: List) -> int:
|
|
161
|
+
"""Calculate compliance score (0-100)"""
|
|
162
|
+
total_checks = len(violations) + len(warnings) + len(passed)
|
|
163
|
+
if total_checks == 0:
|
|
164
|
+
return 100
|
|
165
|
+
|
|
166
|
+
# High severity violations heavily impact score
|
|
167
|
+
high_violations = len([v for v in violations if v.get("severity") == "high"])
|
|
168
|
+
med_violations = len([v for v in violations if v.get("severity") == "medium"])
|
|
169
|
+
low_violations = len([v for v in violations if v.get("severity") == "low"])
|
|
170
|
+
|
|
171
|
+
score = 100
|
|
172
|
+
score -= high_violations * 20
|
|
173
|
+
score -= med_violations * 10
|
|
174
|
+
score -= low_violations * 5
|
|
175
|
+
score -= len(warnings) * 2
|
|
176
|
+
|
|
177
|
+
return max(0, score)
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check Policy task with Evidence Contract - ONLY enforces implemented rules
|
|
3
|
+
V12 Core Hardening
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
import json
|
|
8
|
+
from typing import Dict, List
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from core.registry_v2 import task_registry
|
|
12
|
+
from schemas.requests import CheckPolicyRequest
|
|
13
|
+
from schemas.evidence import (
|
|
14
|
+
PolicyComplianceEvidence, Decision, Violation, ViolationSeverity,
|
|
15
|
+
Evidence, Remediation
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ONLY rules we actually implement
|
|
20
|
+
IMPLEMENTED_RULES = {
|
|
21
|
+
"require_openapi_version",
|
|
22
|
+
"require_info_description",
|
|
23
|
+
"require_security_definition",
|
|
24
|
+
"require_https_only",
|
|
25
|
+
"max_path_depth"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@task_registry.register("check-policy", version="1.0", description="Check API against implemented policy rules")
|
|
30
|
+
def check_policy_handler(request: CheckPolicyRequest) -> PolicyComplianceEvidence:
|
|
31
|
+
"""Validate API spec against ACTUALLY IMPLEMENTED policy rules"""
|
|
32
|
+
|
|
33
|
+
# Get policy - either from file, inline, or defaults
|
|
34
|
+
if request.policy_file:
|
|
35
|
+
policy = load_policy(request.policy_file)
|
|
36
|
+
elif request.policy_inline:
|
|
37
|
+
policy = request.policy_inline
|
|
38
|
+
else:
|
|
39
|
+
policy = get_default_policy()
|
|
40
|
+
|
|
41
|
+
violations = []
|
|
42
|
+
evidence_list = []
|
|
43
|
+
checks_performed = 0
|
|
44
|
+
checks_passed = 0
|
|
45
|
+
|
|
46
|
+
# Process each spec file
|
|
47
|
+
for spec_file in request.spec_files:
|
|
48
|
+
spec = load_spec(spec_file)
|
|
49
|
+
|
|
50
|
+
# ONLY check rules we actually implement
|
|
51
|
+
for rule_name in IMPLEMENTED_RULES:
|
|
52
|
+
if not policy.get("rules", {}).get(rule_name, False):
|
|
53
|
+
continue # Rule not enabled in policy
|
|
54
|
+
|
|
55
|
+
checks_performed += 1
|
|
56
|
+
rule_passed, violation = check_rule(rule_name, spec, policy)
|
|
57
|
+
|
|
58
|
+
if rule_passed:
|
|
59
|
+
checks_passed += 1
|
|
60
|
+
evidence_list.append(Evidence(
|
|
61
|
+
rule=rule_name,
|
|
62
|
+
passed=True,
|
|
63
|
+
details={"file": spec_file, "status": "passed"}
|
|
64
|
+
))
|
|
65
|
+
else:
|
|
66
|
+
violations.append(violation)
|
|
67
|
+
evidence_list.append(Evidence(
|
|
68
|
+
rule=rule_name,
|
|
69
|
+
passed=False,
|
|
70
|
+
details={"file": spec_file, "reason": violation.message}
|
|
71
|
+
))
|
|
72
|
+
|
|
73
|
+
# Calculate compliance score
|
|
74
|
+
compliance_score = int((checks_passed / checks_performed * 100)) if checks_performed > 0 else 100
|
|
75
|
+
|
|
76
|
+
# Determine decision
|
|
77
|
+
if violations:
|
|
78
|
+
high_severity = any(v.severity == ViolationSeverity.HIGH for v in violations)
|
|
79
|
+
if high_severity:
|
|
80
|
+
decision = Decision.FAIL
|
|
81
|
+
exit_code = 1
|
|
82
|
+
else:
|
|
83
|
+
decision = Decision.WARN
|
|
84
|
+
exit_code = 0
|
|
85
|
+
else:
|
|
86
|
+
decision = Decision.PASS
|
|
87
|
+
exit_code = 0
|
|
88
|
+
|
|
89
|
+
# Build summary
|
|
90
|
+
if decision == Decision.PASS:
|
|
91
|
+
summary = f"Policy check passed: All {checks_performed} checks passed"
|
|
92
|
+
elif decision == Decision.WARN:
|
|
93
|
+
summary = f"Policy check passed with warnings: {len(violations)} low-severity issues"
|
|
94
|
+
else:
|
|
95
|
+
summary = f"Policy check failed: {len(violations)} violations found"
|
|
96
|
+
|
|
97
|
+
# Build remediation
|
|
98
|
+
remediation = None
|
|
99
|
+
if violations:
|
|
100
|
+
steps = []
|
|
101
|
+
for v in violations[:3]: # Show top 3 violations
|
|
102
|
+
if v.rule == "require_openapi_version":
|
|
103
|
+
steps.append("Add 'openapi' field with version (e.g., openapi: 3.0.0)")
|
|
104
|
+
elif v.rule == "require_info_description":
|
|
105
|
+
steps.append("Add description field under info section")
|
|
106
|
+
elif v.rule == "require_security_definition":
|
|
107
|
+
steps.append("Define security schemes in components.securitySchemes")
|
|
108
|
+
elif v.rule == "require_https_only":
|
|
109
|
+
steps.append("Update all server URLs to use HTTPS")
|
|
110
|
+
elif v.rule == "max_path_depth":
|
|
111
|
+
steps.append("Simplify API paths to reduce nesting depth")
|
|
112
|
+
|
|
113
|
+
remediation = Remediation(
|
|
114
|
+
summary="Fix policy violations to ensure API compliance",
|
|
115
|
+
steps=steps,
|
|
116
|
+
documentation="https://docs.delimit.ai/policy-rules"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return PolicyComplianceEvidence(
|
|
120
|
+
task="check-policy",
|
|
121
|
+
task_version="1.0",
|
|
122
|
+
decision=decision,
|
|
123
|
+
exit_code=exit_code,
|
|
124
|
+
violations=violations,
|
|
125
|
+
evidence=evidence_list,
|
|
126
|
+
remediation=remediation,
|
|
127
|
+
summary=summary,
|
|
128
|
+
correlation_id=request.correlation_id,
|
|
129
|
+
metrics={
|
|
130
|
+
"files_checked": len(request.spec_files),
|
|
131
|
+
"rules_checked": checks_performed,
|
|
132
|
+
"rules_passed": checks_passed,
|
|
133
|
+
"violations": len(violations)
|
|
134
|
+
},
|
|
135
|
+
compliance_score=compliance_score,
|
|
136
|
+
policy_version="1.0",
|
|
137
|
+
checks_performed=checks_performed,
|
|
138
|
+
checks_passed=checks_passed
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def check_rule(rule_name: str, spec: Dict, policy: Dict) -> tuple[bool, Violation]:
|
|
143
|
+
"""Check a specific rule - ONLY for implemented rules"""
|
|
144
|
+
|
|
145
|
+
severity_map = policy.get("severity", {})
|
|
146
|
+
|
|
147
|
+
if rule_name == "require_openapi_version":
|
|
148
|
+
if "openapi" in spec or "swagger" in spec:
|
|
149
|
+
return True, None
|
|
150
|
+
return False, Violation(
|
|
151
|
+
rule=rule_name,
|
|
152
|
+
severity=ViolationSeverity(severity_map.get(rule_name, "high")),
|
|
153
|
+
message="API specification must include OpenAPI version",
|
|
154
|
+
details={"missing": "openapi field"}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
elif rule_name == "require_info_description":
|
|
158
|
+
if spec.get("info", {}).get("description"):
|
|
159
|
+
return True, None
|
|
160
|
+
return False, Violation(
|
|
161
|
+
rule=rule_name,
|
|
162
|
+
severity=ViolationSeverity(severity_map.get(rule_name, "low")),
|
|
163
|
+
message="API must have a description in info section",
|
|
164
|
+
details={"missing": "info.description"}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
elif rule_name == "require_security_definition":
|
|
168
|
+
has_security = (
|
|
169
|
+
spec.get("security") or
|
|
170
|
+
spec.get("securityDefinitions") or
|
|
171
|
+
spec.get("components", {}).get("securitySchemes")
|
|
172
|
+
)
|
|
173
|
+
if has_security:
|
|
174
|
+
return True, None
|
|
175
|
+
return False, Violation(
|
|
176
|
+
rule=rule_name,
|
|
177
|
+
severity=ViolationSeverity(severity_map.get(rule_name, "high")),
|
|
178
|
+
message="API must define security schemes",
|
|
179
|
+
details={"missing": "security definitions"}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
elif rule_name == "require_https_only":
|
|
183
|
+
servers = spec.get("servers", [])
|
|
184
|
+
if not servers:
|
|
185
|
+
return True, None # No servers defined, pass by default
|
|
186
|
+
|
|
187
|
+
non_https = [s for s in servers if not s.get("url", "").startswith("https://")]
|
|
188
|
+
if non_https:
|
|
189
|
+
return False, Violation(
|
|
190
|
+
rule=rule_name,
|
|
191
|
+
severity=ViolationSeverity(severity_map.get(rule_name, "high")),
|
|
192
|
+
message=f"All servers must use HTTPS ({len(non_https)} use HTTP)",
|
|
193
|
+
details={"non_https_count": str(len(non_https))}
|
|
194
|
+
)
|
|
195
|
+
return True, None
|
|
196
|
+
|
|
197
|
+
elif rule_name == "max_path_depth":
|
|
198
|
+
max_depth = policy.get("rules", {}).get("max_path_depth", 5)
|
|
199
|
+
violations_found = []
|
|
200
|
+
|
|
201
|
+
if "paths" in spec:
|
|
202
|
+
for path in spec["paths"]:
|
|
203
|
+
depth = len([p for p in path.split("/") if p])
|
|
204
|
+
if depth > max_depth:
|
|
205
|
+
violations_found.append(path)
|
|
206
|
+
|
|
207
|
+
if violations_found:
|
|
208
|
+
return False, Violation(
|
|
209
|
+
rule=rule_name,
|
|
210
|
+
severity=ViolationSeverity(severity_map.get(rule_name, "medium")),
|
|
211
|
+
message=f"Paths exceed max depth of {max_depth}",
|
|
212
|
+
path=violations_found[0],
|
|
213
|
+
details={"paths_over_limit": str(len(violations_found))}
|
|
214
|
+
)
|
|
215
|
+
return True, None
|
|
216
|
+
|
|
217
|
+
# Unknown rule - should never happen with IMPLEMENTED_RULES filter
|
|
218
|
+
return True, None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_default_policy() -> Dict:
|
|
222
|
+
"""Get default policy with ONLY implemented rules"""
|
|
223
|
+
return {
|
|
224
|
+
"rules": {
|
|
225
|
+
"require_openapi_version": True,
|
|
226
|
+
"require_info_description": True,
|
|
227
|
+
"require_security_definition": True,
|
|
228
|
+
"require_https_only": True,
|
|
229
|
+
"max_path_depth": 5
|
|
230
|
+
},
|
|
231
|
+
"severity": {
|
|
232
|
+
"require_openapi_version": "high",
|
|
233
|
+
"require_info_description": "low",
|
|
234
|
+
"require_security_definition": "high",
|
|
235
|
+
"require_https_only": "high",
|
|
236
|
+
"max_path_depth": "medium"
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def load_spec(file_path: str) -> Dict:
|
|
242
|
+
"""Load API specification"""
|
|
243
|
+
path = Path(file_path)
|
|
244
|
+
with path.open('r') as f:
|
|
245
|
+
if path.suffix in ['.yaml', '.yml']:
|
|
246
|
+
return yaml.safe_load(f)
|
|
247
|
+
else:
|
|
248
|
+
return json.load(f)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def load_policy(policy_path: str) -> Dict:
|
|
252
|
+
"""Load policy rules from file"""
|
|
253
|
+
path = Path(policy_path)
|
|
254
|
+
with path.open('r') as f:
|
|
255
|
+
return yaml.safe_load(f)
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check Policy task with Evidence Contract - V12 Final
|
|
3
|
+
ONLY enforces implemented rules with Pydantic v2
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
import json
|
|
8
|
+
from typing import Dict, List
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from core.registry_v3 import task_registry
|
|
12
|
+
from schemas.requests_v2 import CheckPolicyRequest
|
|
13
|
+
from schemas.evidence import (
|
|
14
|
+
PolicyComplianceEvidence, Decision, Violation, ViolationSeverity,
|
|
15
|
+
Evidence, Remediation
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ONLY rules we actually implement
|
|
20
|
+
IMPLEMENTED_RULES = {
|
|
21
|
+
"require_openapi_version",
|
|
22
|
+
"require_info_description",
|
|
23
|
+
"require_security_definition",
|
|
24
|
+
"require_https_only",
|
|
25
|
+
"max_path_depth"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@task_registry.register("check-policy", task_version="1.0", description="Check API against implemented policy rules")
|
|
30
|
+
def check_policy_handler(request: CheckPolicyRequest) -> PolicyComplianceEvidence:
|
|
31
|
+
"""Validate API spec against ACTUALLY IMPLEMENTED policy rules"""
|
|
32
|
+
|
|
33
|
+
# Get policy - either from file, inline, or defaults
|
|
34
|
+
if request.policy_file:
|
|
35
|
+
policy = load_policy(request.policy_file)
|
|
36
|
+
elif request.policy_inline:
|
|
37
|
+
policy = request.policy_inline
|
|
38
|
+
else:
|
|
39
|
+
policy = get_default_policy()
|
|
40
|
+
|
|
41
|
+
violations = []
|
|
42
|
+
evidence_list = []
|
|
43
|
+
checks_performed = 0
|
|
44
|
+
checks_passed = 0
|
|
45
|
+
|
|
46
|
+
# Process each spec file
|
|
47
|
+
for spec_file in request.spec_files:
|
|
48
|
+
spec = load_spec(spec_file)
|
|
49
|
+
|
|
50
|
+
# ONLY check rules we actually implement
|
|
51
|
+
for rule_name in IMPLEMENTED_RULES:
|
|
52
|
+
if not policy.get("rules", {}).get(rule_name, False):
|
|
53
|
+
continue # Rule not enabled in policy
|
|
54
|
+
|
|
55
|
+
checks_performed += 1
|
|
56
|
+
rule_passed, violation = check_rule(rule_name, spec, policy)
|
|
57
|
+
|
|
58
|
+
if rule_passed:
|
|
59
|
+
checks_passed += 1
|
|
60
|
+
evidence_list.append(Evidence(
|
|
61
|
+
rule=rule_name,
|
|
62
|
+
passed=True,
|
|
63
|
+
details={"file": spec_file, "status": "passed"}
|
|
64
|
+
))
|
|
65
|
+
else:
|
|
66
|
+
violations.append(violation)
|
|
67
|
+
evidence_list.append(Evidence(
|
|
68
|
+
rule=rule_name,
|
|
69
|
+
passed=False,
|
|
70
|
+
details={"file": spec_file, "reason": violation.message}
|
|
71
|
+
))
|
|
72
|
+
|
|
73
|
+
# Calculate compliance score
|
|
74
|
+
compliance_score = int((checks_passed / checks_performed * 100)) if checks_performed > 0 else 100
|
|
75
|
+
|
|
76
|
+
# Determine decision
|
|
77
|
+
if violations:
|
|
78
|
+
high_severity = any(v.severity == ViolationSeverity.HIGH for v in violations)
|
|
79
|
+
if high_severity:
|
|
80
|
+
decision = Decision.FAIL
|
|
81
|
+
exit_code = 1
|
|
82
|
+
else:
|
|
83
|
+
decision = Decision.WARN
|
|
84
|
+
exit_code = 0
|
|
85
|
+
else:
|
|
86
|
+
decision = Decision.PASS
|
|
87
|
+
exit_code = 0
|
|
88
|
+
|
|
89
|
+
# Build summary
|
|
90
|
+
if decision == Decision.PASS:
|
|
91
|
+
summary = f"Policy check passed: All {checks_performed} checks passed"
|
|
92
|
+
elif decision == Decision.WARN:
|
|
93
|
+
summary = f"Policy check passed with warnings: {len(violations)} low-severity issues"
|
|
94
|
+
else:
|
|
95
|
+
summary = f"Policy check failed: {len(violations)} violations found"
|
|
96
|
+
|
|
97
|
+
# Build remediation
|
|
98
|
+
remediation = None
|
|
99
|
+
if violations:
|
|
100
|
+
steps = []
|
|
101
|
+
for v in violations[:3]: # Show top 3 violations
|
|
102
|
+
if v.rule == "require_openapi_version":
|
|
103
|
+
steps.append("Add 'openapi' field with version (e.g., openapi: 3.0.0)")
|
|
104
|
+
elif v.rule == "require_info_description":
|
|
105
|
+
steps.append("Add description field under info section")
|
|
106
|
+
elif v.rule == "require_security_definition":
|
|
107
|
+
steps.append("Define security schemes in components.securitySchemes")
|
|
108
|
+
elif v.rule == "require_https_only":
|
|
109
|
+
steps.append("Update all server URLs to use HTTPS")
|
|
110
|
+
elif v.rule == "max_path_depth":
|
|
111
|
+
steps.append("Simplify API paths to reduce nesting depth")
|
|
112
|
+
|
|
113
|
+
remediation = Remediation(
|
|
114
|
+
summary="Fix policy violations to ensure API compliance",
|
|
115
|
+
steps=steps,
|
|
116
|
+
documentation="https://docs.delimit.ai/policy-rules"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return PolicyComplianceEvidence(
|
|
120
|
+
task="check-policy",
|
|
121
|
+
task_version="1.0",
|
|
122
|
+
decision=decision,
|
|
123
|
+
exit_code=exit_code,
|
|
124
|
+
violations=violations,
|
|
125
|
+
evidence=evidence_list,
|
|
126
|
+
remediation=remediation,
|
|
127
|
+
summary=summary,
|
|
128
|
+
correlation_id=request.correlation_id,
|
|
129
|
+
metrics={
|
|
130
|
+
"files_checked": len(request.spec_files),
|
|
131
|
+
"rules_checked": checks_performed,
|
|
132
|
+
"rules_passed": checks_passed,
|
|
133
|
+
"violations": len(violations)
|
|
134
|
+
},
|
|
135
|
+
compliance_score=compliance_score,
|
|
136
|
+
policy_version="1.0",
|
|
137
|
+
checks_performed=checks_performed,
|
|
138
|
+
checks_passed=checks_passed
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def check_rule(rule_name: str, spec: Dict, policy: Dict) -> tuple[bool, Violation]:
|
|
143
|
+
"""Check a specific rule - ONLY for implemented rules"""
|
|
144
|
+
|
|
145
|
+
severity_map = policy.get("severity", {})
|
|
146
|
+
|
|
147
|
+
if rule_name == "require_openapi_version":
|
|
148
|
+
if "openapi" in spec or "swagger" in spec:
|
|
149
|
+
return True, None
|
|
150
|
+
return False, Violation(
|
|
151
|
+
rule=rule_name,
|
|
152
|
+
severity=ViolationSeverity(severity_map.get(rule_name, "high")),
|
|
153
|
+
message="API specification must include OpenAPI version",
|
|
154
|
+
details={"missing": "openapi field"}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
elif rule_name == "require_info_description":
|
|
158
|
+
if spec.get("info", {}).get("description"):
|
|
159
|
+
return True, None
|
|
160
|
+
return False, Violation(
|
|
161
|
+
rule=rule_name,
|
|
162
|
+
severity=ViolationSeverity(severity_map.get(rule_name, "low")),
|
|
163
|
+
message="API must have a description in info section",
|
|
164
|
+
details={"missing": "info.description"}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
elif rule_name == "require_security_definition":
|
|
168
|
+
has_security = (
|
|
169
|
+
spec.get("security") or
|
|
170
|
+
spec.get("securityDefinitions") or
|
|
171
|
+
spec.get("components", {}).get("securitySchemes")
|
|
172
|
+
)
|
|
173
|
+
if has_security:
|
|
174
|
+
return True, None
|
|
175
|
+
return False, Violation(
|
|
176
|
+
rule=rule_name,
|
|
177
|
+
severity=ViolationSeverity(severity_map.get(rule_name, "high")),
|
|
178
|
+
message="API must define security schemes",
|
|
179
|
+
details={"missing": "security definitions"}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
elif rule_name == "require_https_only":
|
|
183
|
+
servers = spec.get("servers", [])
|
|
184
|
+
if not servers:
|
|
185
|
+
return True, None # No servers defined, pass by default
|
|
186
|
+
|
|
187
|
+
non_https = [s for s in servers if not s.get("url", "").startswith("https://")]
|
|
188
|
+
if non_https:
|
|
189
|
+
return False, Violation(
|
|
190
|
+
rule=rule_name,
|
|
191
|
+
severity=ViolationSeverity(severity_map.get(rule_name, "high")),
|
|
192
|
+
message=f"All servers must use HTTPS ({len(non_https)} use HTTP)",
|
|
193
|
+
details={"non_https_count": str(len(non_https))}
|
|
194
|
+
)
|
|
195
|
+
return True, None
|
|
196
|
+
|
|
197
|
+
elif rule_name == "max_path_depth":
|
|
198
|
+
max_depth = policy.get("rules", {}).get("max_path_depth", 5)
|
|
199
|
+
violations_found = []
|
|
200
|
+
|
|
201
|
+
if "paths" in spec:
|
|
202
|
+
for path in spec["paths"]:
|
|
203
|
+
depth = len([p for p in path.split("/") if p])
|
|
204
|
+
if depth > max_depth:
|
|
205
|
+
violations_found.append(path)
|
|
206
|
+
|
|
207
|
+
if violations_found:
|
|
208
|
+
return False, Violation(
|
|
209
|
+
rule=rule_name,
|
|
210
|
+
severity=ViolationSeverity(severity_map.get(rule_name, "medium")),
|
|
211
|
+
message=f"Paths exceed max depth of {max_depth}",
|
|
212
|
+
path=violations_found[0],
|
|
213
|
+
details={"paths_over_limit": str(len(violations_found))}
|
|
214
|
+
)
|
|
215
|
+
return True, None
|
|
216
|
+
|
|
217
|
+
# Unknown rule - should never happen with IMPLEMENTED_RULES filter
|
|
218
|
+
return True, None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_default_policy() -> Dict:
|
|
222
|
+
"""Get default policy with ONLY implemented rules"""
|
|
223
|
+
return {
|
|
224
|
+
"rules": {
|
|
225
|
+
"require_openapi_version": True,
|
|
226
|
+
"require_info_description": True,
|
|
227
|
+
"require_security_definition": True,
|
|
228
|
+
"require_https_only": True,
|
|
229
|
+
"max_path_depth": 5
|
|
230
|
+
},
|
|
231
|
+
"severity": {
|
|
232
|
+
"require_openapi_version": "high",
|
|
233
|
+
"require_info_description": "low",
|
|
234
|
+
"require_security_definition": "high",
|
|
235
|
+
"require_https_only": "high",
|
|
236
|
+
"max_path_depth": "medium"
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def load_spec(file_path: str) -> Dict:
|
|
242
|
+
"""Load API specification"""
|
|
243
|
+
path = Path(file_path)
|
|
244
|
+
with path.open('r') as f:
|
|
245
|
+
if path.suffix in ['.yaml', '.yml']:
|
|
246
|
+
return yaml.safe_load(f)
|
|
247
|
+
else:
|
|
248
|
+
return json.load(f)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def load_policy(policy_path: str) -> Dict:
|
|
252
|
+
"""Load policy rules from file"""
|
|
253
|
+
path = Path(policy_path)
|
|
254
|
+
with path.open('r') as f:
|
|
255
|
+
return yaml.safe_load(f)
|