Initial commit
This commit is contained in:
553
skills/appsec/api-spectral/references/custom_rules_guide.md
Normal file
553
skills/appsec/api-spectral/references/custom_rules_guide.md
Normal file
@@ -0,0 +1,553 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user