554 lines
12 KiB
Markdown
554 lines
12 KiB
Markdown
# Spectral Custom Rules Development Guide
|
|
|
|
This guide covers creating custom security rules for Spectral to enforce organization-specific API security standards.
|
|
|
|
## Table of Contents
|
|
|
|
- [Rule Structure](#rule-structure)
|
|
- [JSONPath Expressions](#jsonpath-expressions)
|
|
- [Built-in Functions](#built-in-functions)
|
|
- [Security Rule Examples](#security-rule-examples)
|
|
- [Testing Custom Rules](#testing-custom-rules)
|
|
- [Best Practices](#best-practices)
|
|
|
|
## Rule Structure
|
|
|
|
Every Spectral rule consists of:
|
|
|
|
```yaml
|
|
rules:
|
|
rule-name:
|
|
description: Human-readable description
|
|
severity: error|warn|info|hint
|
|
given: JSONPath expression targeting specific parts of spec
|
|
then:
|
|
- field: property to check (optional)
|
|
function: validation function
|
|
functionOptions: function-specific options
|
|
message: Error message shown when rule fails
|
|
```
|
|
|
|
### Severity Levels
|
|
|
|
- **error**: Critical security issues that must be fixed
|
|
- **warn**: Important security recommendations
|
|
- **info**: Best practices and suggestions
|
|
- **hint**: Style guide and documentation improvements
|
|
|
|
## JSONPath Expressions
|
|
|
|
### Basic Path Selection
|
|
|
|
```yaml
|
|
# Target all paths
|
|
given: $.paths[*]
|
|
|
|
# Target all GET operations
|
|
given: $.paths[*].get
|
|
|
|
# Target all HTTP methods
|
|
given: $.paths[*][get,post,put,patch,delete]
|
|
|
|
# Target security schemes
|
|
given: $.components.securitySchemes[*]
|
|
|
|
# Target all schemas
|
|
given: $.components.schemas[*]
|
|
```
|
|
|
|
### Advanced Filters
|
|
|
|
```yaml
|
|
# Filter by property value
|
|
given: $.paths[*][?(@.security)]
|
|
|
|
# Filter objects by type
|
|
given: $.components.schemas[?(@.type == 'object')]
|
|
|
|
# Filter parameters by location
|
|
given: $.paths[*][*].parameters[?(@.in == 'query')]
|
|
|
|
# Regular expression matching
|
|
given: $.paths[*][*].parameters[?(@.name =~ /^(id|.*_id)$/i)]
|
|
|
|
# Nested property access
|
|
given: $.paths[*][*].responses[?(@property >= 400)]
|
|
```
|
|
|
|
## Built-in Functions
|
|
|
|
### truthy / falsy
|
|
|
|
Check if field exists or doesn't exist:
|
|
|
|
```yaml
|
|
# Require field to exist
|
|
then:
|
|
- field: security
|
|
function: truthy
|
|
|
|
# Require field to not exist
|
|
then:
|
|
- field: additionalProperties
|
|
function: falsy
|
|
```
|
|
|
|
### pattern
|
|
|
|
Match string against regex pattern:
|
|
|
|
```yaml
|
|
# Match HTTPS URLs
|
|
then:
|
|
function: pattern
|
|
functionOptions:
|
|
match: "^https://"
|
|
|
|
# Ensure no sensitive terms
|
|
then:
|
|
function: pattern
|
|
functionOptions:
|
|
notMatch: "(password|secret|api[_-]?key)"
|
|
```
|
|
|
|
### enumeration
|
|
|
|
Restrict to specific values:
|
|
|
|
```yaml
|
|
# Require specific auth types
|
|
then:
|
|
field: type
|
|
function: enumeration
|
|
functionOptions:
|
|
values: [apiKey, oauth2, openIdConnect]
|
|
```
|
|
|
|
### length
|
|
|
|
Validate string/array length:
|
|
|
|
```yaml
|
|
# Minimum description length
|
|
then:
|
|
field: description
|
|
function: length
|
|
functionOptions:
|
|
min: 10
|
|
max: 500
|
|
```
|
|
|
|
### schema
|
|
|
|
Validate against JSON Schema:
|
|
|
|
```yaml
|
|
# Require specific object structure
|
|
then:
|
|
function: schema
|
|
functionOptions:
|
|
schema:
|
|
type: object
|
|
required: [error, message]
|
|
properties:
|
|
error:
|
|
type: string
|
|
message:
|
|
type: string
|
|
```
|
|
|
|
### alphabetical
|
|
|
|
Ensure alphabetical ordering:
|
|
|
|
```yaml
|
|
# Require alphabetically sorted tags
|
|
then:
|
|
field: tags
|
|
function: alphabetical
|
|
```
|
|
|
|
## Security Rule Examples
|
|
|
|
### Prevent PII in URL Parameters
|
|
|
|
```yaml
|
|
no-pii-in-query-params:
|
|
description: Query parameters must not contain PII
|
|
severity: error
|
|
given: $.paths[*][*].parameters[?(@.in == 'query')].name
|
|
then:
|
|
function: pattern
|
|
functionOptions:
|
|
notMatch: "(?i)(ssn|social.?security|credit.?card|password|passport|driver.?license|tax.?id|national.?id)"
|
|
message: "Query parameter names suggest PII - use request body instead"
|
|
```
|
|
|
|
### Require API Key for Authentication
|
|
|
|
```yaml
|
|
require-api-key-security:
|
|
description: APIs must use API key authentication
|
|
severity: error
|
|
given: $.components.securitySchemes
|
|
then:
|
|
function: schema
|
|
functionOptions:
|
|
schema:
|
|
type: object
|
|
minProperties: 1
|
|
patternProperties:
|
|
".*":
|
|
anyOf:
|
|
- properties:
|
|
type:
|
|
const: apiKey
|
|
- properties:
|
|
type:
|
|
const: oauth2
|
|
- properties:
|
|
type:
|
|
const: openIdConnect
|
|
message: "API must define apiKey, OAuth2, or OpenID Connect security"
|
|
```
|
|
|
|
### Enforce Rate Limiting Headers
|
|
|
|
```yaml
|
|
rate-limit-headers-present:
|
|
description: Responses should include rate limit headers
|
|
severity: warn
|
|
given: $.paths[*][get,post,put,patch,delete].responses[?(@property == '200' || @property == '201')].headers
|
|
then:
|
|
function: schema
|
|
functionOptions:
|
|
schema:
|
|
type: object
|
|
anyOf:
|
|
- required: [X-RateLimit-Limit]
|
|
- required: [X-Rate-Limit-Limit]
|
|
message: "Include rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining) in success responses"
|
|
```
|
|
|
|
### Detect Missing Authorization for Sensitive Operations
|
|
|
|
```yaml
|
|
sensitive-operations-require-security:
|
|
description: Sensitive operations must have security requirements
|
|
severity: error
|
|
given: $.paths[*][post,put,patch,delete]
|
|
then:
|
|
- field: security
|
|
function: truthy
|
|
message: "Write operations must have security requirements defined"
|
|
```
|
|
|
|
### Prevent Verbose Error Messages
|
|
|
|
```yaml
|
|
no-verbose-error-responses:
|
|
description: Error responses should not expose internal details
|
|
severity: warn
|
|
given: $.paths[*][*].responses[?(@property >= 500)].content.application/json.schema.properties
|
|
then:
|
|
function: schema
|
|
functionOptions:
|
|
schema:
|
|
type: object
|
|
not:
|
|
anyOf:
|
|
- required: [stack_trace]
|
|
- required: [stackTrace]
|
|
- required: [debug_info]
|
|
- required: [internal_message]
|
|
message: "5xx error responses should not expose stack traces or internal details"
|
|
```
|
|
|
|
### Require Audit Fields in Schemas
|
|
|
|
```yaml
|
|
require-audit-fields:
|
|
description: Data models should include audit fields
|
|
severity: info
|
|
given: $.components.schemas[?(@.type == 'object' && @.properties)]
|
|
then:
|
|
function: schema
|
|
functionOptions:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
properties:
|
|
type: object
|
|
anyOf:
|
|
- required: [created_at, updated_at]
|
|
- required: [createdAt, updatedAt]
|
|
message: "Consider adding audit fields (created_at, updated_at) to data models"
|
|
```
|
|
|
|
### Detect Insecure Content Types
|
|
|
|
```yaml
|
|
no-insecure-content-types:
|
|
description: Avoid insecure content types
|
|
severity: warn
|
|
given: $.paths[*][*].requestBody.content
|
|
then:
|
|
function: schema
|
|
functionOptions:
|
|
schema:
|
|
type: object
|
|
not:
|
|
required: [text/html, text/xml, application/x-www-form-urlencoded]
|
|
message: "Prefer application/json over HTML, XML, or form-encoded content types"
|
|
```
|
|
|
|
### Validate JWT Security Configuration
|
|
|
|
```yaml
|
|
jwt-proper-configuration:
|
|
description: JWT bearer authentication should be properly configured
|
|
severity: error
|
|
given: $.components.securitySchemes[?(@.type == 'http' && @.scheme == 'bearer')]
|
|
then:
|
|
- field: bearerFormat
|
|
function: pattern
|
|
functionOptions:
|
|
match: "^JWT$"
|
|
message: "Bearer authentication should specify 'JWT' as bearerFormat"
|
|
```
|
|
|
|
### Require CORS Documentation
|
|
|
|
```yaml
|
|
cors-options-documented:
|
|
description: CORS preflight endpoints should be documented
|
|
severity: warn
|
|
given: $.paths[*]
|
|
then:
|
|
function: schema
|
|
functionOptions:
|
|
schema:
|
|
type: object
|
|
if:
|
|
properties:
|
|
get:
|
|
type: object
|
|
then:
|
|
properties:
|
|
options:
|
|
type: object
|
|
required: [responses]
|
|
message: "Document OPTIONS method for CORS preflight requests"
|
|
```
|
|
|
|
### Prevent Numeric IDs in URLs
|
|
|
|
```yaml
|
|
prefer-uuid-over-numeric-ids:
|
|
description: Use UUIDs instead of numeric IDs to prevent enumeration
|
|
severity: info
|
|
given: $.paths.*~
|
|
then:
|
|
function: pattern
|
|
functionOptions:
|
|
notMatch: "\\{id\\}|\\{.*_id\\}"
|
|
message: "Consider using UUIDs instead of numeric IDs to prevent enumeration attacks"
|
|
```
|
|
|
|
## Testing Custom Rules
|
|
|
|
### Create Test Specifications
|
|
|
|
```yaml
|
|
# test-specs/valid-auth.yaml
|
|
openapi: 3.0.0
|
|
info:
|
|
title: Valid API
|
|
version: 1.0.0
|
|
components:
|
|
securitySchemes:
|
|
apiKey:
|
|
type: apiKey
|
|
in: header
|
|
name: X-API-Key
|
|
security:
|
|
- apiKey: []
|
|
```
|
|
|
|
```yaml
|
|
# test-specs/invalid-auth.yaml
|
|
openapi: 3.0.0
|
|
info:
|
|
title: Invalid API
|
|
version: 1.0.0
|
|
components:
|
|
securitySchemes:
|
|
basicAuth:
|
|
type: http
|
|
scheme: basic
|
|
security:
|
|
- basicAuth: []
|
|
```
|
|
|
|
### Test Rules
|
|
|
|
```bash
|
|
# Test custom ruleset
|
|
spectral lint test-specs/valid-auth.yaml --ruleset .spectral-custom.yaml
|
|
|
|
# Expected: No errors
|
|
|
|
spectral lint test-specs/invalid-auth.yaml --ruleset .spectral-custom.yaml
|
|
|
|
# Expected: Error about HTTP Basic auth
|
|
```
|
|
|
|
### Automated Testing Script
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
# test-rules.sh - Test custom Spectral rules
|
|
|
|
RULESET=".spectral-custom.yaml"
|
|
TEST_DIR="test-specs"
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
for spec in "$TEST_DIR"/*.yaml; do
|
|
echo "Testing: $spec"
|
|
|
|
if spectral lint "$spec" --ruleset "$RULESET" > /dev/null 2>&1; then
|
|
if [[ "$spec" == *"valid"* ]]; then
|
|
echo " ✓ PASS (correctly validated)"
|
|
((PASS++))
|
|
else
|
|
echo " ✗ FAIL (should have detected issues)"
|
|
((FAIL++))
|
|
fi
|
|
else
|
|
if [[ "$spec" == *"invalid"* ]]; then
|
|
echo " ✓ PASS (correctly detected issues)"
|
|
((PASS++))
|
|
else
|
|
echo " ✗ FAIL (false positive)"
|
|
((FAIL++))
|
|
fi
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "Results: $PASS passed, $FAIL failed"
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### 1. Start with Built-in Rules
|
|
|
|
Extend existing rulesets instead of starting from scratch:
|
|
|
|
```yaml
|
|
extends: ["spectral:oas", "spectral:asyncapi"]
|
|
|
|
rules:
|
|
# Add custom rules here
|
|
custom-security-rule:
|
|
# ...
|
|
```
|
|
|
|
### 2. Use Descriptive Names
|
|
|
|
Rule names should clearly indicate what they check:
|
|
|
|
```yaml
|
|
# Good
|
|
no-pii-in-query-params:
|
|
require-https-servers:
|
|
jwt-bearer-format-required:
|
|
|
|
# Bad
|
|
check-params:
|
|
security-rule-1:
|
|
validate-auth:
|
|
```
|
|
|
|
### 3. Provide Actionable Messages
|
|
|
|
```yaml
|
|
# Good
|
|
message: "Query parameters must not contain PII (ssn, credit_card) - use request body instead"
|
|
|
|
# Bad
|
|
message: "Invalid parameter"
|
|
```
|
|
|
|
### 4. Choose Appropriate Severity
|
|
|
|
```yaml
|
|
# error - Must fix (security vulnerabilities)
|
|
severity: error
|
|
|
|
# warn - Should fix (security best practices)
|
|
severity: warn
|
|
|
|
# info - Consider fixing (recommendations)
|
|
severity: info
|
|
|
|
# hint - Nice to have (style guide)
|
|
severity: hint
|
|
```
|
|
|
|
### 5. Document Rule Rationale
|
|
|
|
```yaml
|
|
rules:
|
|
no-numeric-ids:
|
|
description: |
|
|
Use UUIDs instead of auto-incrementing numeric IDs in URLs to prevent
|
|
enumeration attacks where attackers can guess valid IDs sequentially.
|
|
This follows OWASP API Security best practices for API1:2023.
|
|
severity: warn
|
|
# ...
|
|
```
|
|
|
|
### 6. Use Rule Overrides for Exceptions
|
|
|
|
```yaml
|
|
# Allow specific paths to violate rules
|
|
overrides:
|
|
- files: ["**/internal-api.yaml"]
|
|
rules:
|
|
require-https-servers: off
|
|
|
|
- files: ["**/admin-api.yaml"]
|
|
rules:
|
|
no-http-basic-auth: warn # Downgrade to warning
|
|
```
|
|
|
|
### 7. Organize Rules by Category
|
|
|
|
```yaml
|
|
# .spectral.yaml - Main ruleset
|
|
extends:
|
|
- .spectral-auth.yaml # Authentication rules
|
|
- .spectral-authz.yaml # Authorization rules
|
|
- .spectral-data.yaml # Data protection rules
|
|
- .spectral-owasp.yaml # OWASP mappings
|
|
```
|
|
|
|
### 8. Version Control Your Rulesets
|
|
|
|
```bash
|
|
# Track ruleset evolution
|
|
git log -p .spectral.yaml
|
|
|
|
# Tag stable ruleset versions
|
|
git tag -a ruleset-v1.0 -m "Production-ready security ruleset"
|
|
```
|
|
|
|
## Additional Resources
|
|
|
|
- [Spectral Rulesets Documentation](https://docs.stoplight.io/docs/spectral/docs/getting-started/rulesets.md)
|
|
- [JSONPath Online Evaluator](https://jsonpath.com/)
|
|
- [Custom Functions Guide](./custom_functions.md)
|
|
- [OWASP API Security Mappings](./owasp_api_mappings.md)
|