mirror of
https://github.com/splunk/security_content
synced 2026-06-08 17:32:49 +00:00
3953a43f05
* Create response_plan directory * Update directory name * Copy response_templates artifacts to dist/api * Add response-templates schema validation workflow * Add feature branch for testing purpose * Update endpoint to playground * Revert back debug changes * Move scripts to workflows * Remove manual check in * Add sorting for version and template name * Raise exception when file name not match * Add indentation for json output * Add debug option to dump json schema * Generate merged templates at runtime * Rename openAPI spec yaml to yml * Move validation to build.yml * Use stem to get file name * Fix python package install * Update version sorting using int * Update openAPI spec for version * Move build response templates to separate workflow * Fix naming in build-response-templates.yml * Update response templates to the ones for first release * Fix naming of response templates * Response templates to be added by response plan team * Keep response_templates directory * Skip .gitkeep checking when check non-json files * Remove the .gitkeep * Initial version of Response Templates * Initial version of Response Templates * Initial version of Response Templates * Revert "Initial version of Response Templates" This reverts commit3a174dd02e. * Revert "Initial version of Response Templates" This reverts commit26fa66ddde. * Revert "Initial version of Response Templates" This reverts commit6014b4870b. * Initial version of Response Templates * Initial version of Response Templates * Update and rename AccountCompromise_v14.json to AccountCompromise_v2.json * Update and rename DataBreach_v15.json to DataBreach_v2.json * Update and rename GenericIncidentResponse_v13.json to GenericIncidentResponse_v2.json * Update and rename NIST80061_v14.json to NIST80061_v2.json * Update and rename NetworkIndicatorEnrichment_v6.json to NetworkIndicatorEnrichment_v2.json * Update and rename SelfReplicatingMalware_v14.json to SelfReplicatingMalware_v2.json * Update and rename SuspiciousEmail_v35.json to SuspiciousEmail_v2.json * Update and rename VulnerabilityDisclosure_v10.json to VulnerabilityDisclosure_v2.json * Add comments --------- Co-authored-by: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Co-authored-by: Christian Cloutier <ccloutier@splunk.com> Co-authored-by: kbouchard <47464052+kbouchardherjavecgroup@users.noreply.github.com> Co-authored-by: Bhavin Patel <bhavin.j.patel91@gmail.com>
272 lines
9.8 KiB
Python
272 lines
9.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Response template validator for CI/CD pipelines.
|
|
|
|
This script validates response template JSON files against OpenAPI schemas defined in
|
|
mcopenapi_public.yml. It ensures template structure, field types, and required properties
|
|
match the API contract before deployment. Supports validation of individual templates,
|
|
merged template arrays, and manifest files. Exits with non-zero status on validation
|
|
failures to block CI builds with invalid templates.
|
|
|
|
Example usage:
|
|
# Validate individual template files (from repo root)
|
|
python .github/workflows/response_templates/validate_response_templates.py \
|
|
-d response_templates/ -s .github/workflows/response_templates/mcopenapi_public.yml
|
|
|
|
# Validate manifest and merged templates
|
|
python .github/workflows/response_templates/validate_response_templates.py \
|
|
-d response_templates/ -s .github/workflows/response_templates/mcopenapi_public.yml \
|
|
-m output/manifest.json --merged-dir output/
|
|
|
|
# Enable debug mode to dump resolved schemas
|
|
python .github/workflows/response_templates/validate_response_templates.py \
|
|
-d response_templates/ -s .github/workflows/response_templates/mcopenapi_public.yml --debug
|
|
"""
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Dict, Any, Tuple
|
|
|
|
import yaml
|
|
from jsonschema import Draft7Validator
|
|
|
|
|
|
def load_openapi_schema(yaml_path: Path, schema_name: str = 'ResponseTemplate', debug: bool = False) -> Dict[str, Any]:
|
|
"""Load the OpenAPI YAML file and extract the specified schema."""
|
|
with open(yaml_path, 'r') as f:
|
|
openapi_spec = yaml.safe_load(f)
|
|
|
|
# Extract the specified schema
|
|
target_schema = openapi_spec['components']['schemas'][schema_name]
|
|
|
|
# Resolve references to other schemas
|
|
schemas = openapi_spec['components']['schemas']
|
|
|
|
# We need to build a complete schema by resolving $ref
|
|
def resolve_refs(schema_obj: Any, schemas_dict: Dict) -> Any:
|
|
"""Recursively resolve $ref in schema objects."""
|
|
if isinstance(schema_obj, dict):
|
|
if '$ref' in schema_obj:
|
|
# Extract schema name from reference like "#/components/schemas/ResponseTemplatePhase"
|
|
ref_path = schema_obj['$ref'].split('/')[-1]
|
|
return resolve_refs(schemas_dict.get(ref_path, {}), schemas_dict)
|
|
else:
|
|
return {k: resolve_refs(v, schemas_dict) for k, v in schema_obj.items()}
|
|
elif isinstance(schema_obj, list):
|
|
return [resolve_refs(item, schemas_dict) for item in schema_obj]
|
|
else:
|
|
return schema_obj
|
|
|
|
resolved_schema = resolve_refs(target_schema, schemas)
|
|
|
|
# Add JSON Schema draft version
|
|
resolved_schema['$schema'] = 'http://json-schema.org/draft-07/schema#'
|
|
|
|
# Debug: dump resolved schema to file (only once per schema)
|
|
if debug:
|
|
debug_file = Path(f"debug_{schema_name}_schema.json")
|
|
if not debug_file.exists():
|
|
with open(debug_file, 'w') as f:
|
|
json.dump(resolved_schema, f, indent=2)
|
|
print(f"🐛 Debug: Resolved schema dumped to {debug_file}")
|
|
else:
|
|
print(f"🐛 Debug: Resolved schema already exists at {debug_file}")
|
|
|
|
return resolved_schema
|
|
|
|
|
|
def validate_json_file(json_path: Path, schema: Dict[str, Any]) -> Tuple[bool, str]:
|
|
"""
|
|
Validate a JSON file against the provided schema.
|
|
Returns (is_valid, error_message)
|
|
"""
|
|
try:
|
|
with open(json_path, 'r') as f:
|
|
json_data = json.load(f)
|
|
|
|
# Validate against schema
|
|
validator = Draft7Validator(schema)
|
|
errors = sorted(validator.iter_errors(json_data), key=lambda e: e.path)
|
|
|
|
if errors:
|
|
error_messages = []
|
|
for error in errors:
|
|
path = '.'.join(str(p) for p in error.path)
|
|
error_messages.append(f" - Path '{path}': {error.message}")
|
|
return False, '\n'.join(error_messages)
|
|
|
|
return True, "Valid"
|
|
|
|
except json.JSONDecodeError as e:
|
|
return False, f"JSON parsing error: {e}"
|
|
except Exception as e:
|
|
return False, f"Unexpected error: {e}"
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Validate response_templates JSON files against the ResponseTemplate schema"
|
|
)
|
|
parser.add_argument(
|
|
'-d', '--directory',
|
|
type=str,
|
|
default='.',
|
|
help='Directory containing response template JSON files'
|
|
)
|
|
parser.add_argument(
|
|
'-s', '--schema',
|
|
type=str,
|
|
default='mcopenapi_public.yml',
|
|
help='Path to the OpenAPI YAML schema file'
|
|
)
|
|
parser.add_argument(
|
|
'-m', '--manifest',
|
|
type=str,
|
|
help='Path to manifest.json file to validate against ResponseTemplateManifest schema'
|
|
)
|
|
parser.add_argument(
|
|
'--merged-dir',
|
|
type=str,
|
|
help='Directory containing merged response template JSON files to validate against ResponseTemplateMerged schema'
|
|
)
|
|
parser.add_argument(
|
|
'--debug',
|
|
action='store_true',
|
|
help='Dump resolved schemas to JSON files for debugging'
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Resolve paths
|
|
schema_path = Path(args.schema)
|
|
templates_dir = Path(args.directory)
|
|
|
|
if not schema_path.exists():
|
|
print(f"❌ Error: Schema file not found: {schema_path}")
|
|
sys.exit(1)
|
|
|
|
if not templates_dir.exists():
|
|
print(f"❌ Error: Templates directory not found: {templates_dir}")
|
|
sys.exit(1)
|
|
|
|
validation_results = []
|
|
|
|
# Validate manifest if provided
|
|
if args.manifest:
|
|
manifest_path = Path(args.manifest)
|
|
if not manifest_path.exists():
|
|
print(f"❌ Error: Manifest file not found: {manifest_path}")
|
|
sys.exit(1)
|
|
|
|
print(f"📋 Loading ResponseTemplateManifest schema from {schema_path}")
|
|
try:
|
|
manifest_schema = load_openapi_schema(schema_path, 'ResponseTemplateManifest', debug=args.debug)
|
|
print(f"✅ Manifest schema loaded successfully")
|
|
except Exception as e:
|
|
print(f"❌ Error loading manifest schema: {e}")
|
|
sys.exit(1)
|
|
|
|
print(f"\n🔍 Validating manifest file: {manifest_path.name}\n")
|
|
print(f"Validating {manifest_path.name}...", end=" ")
|
|
is_valid, message = validate_json_file(manifest_path, manifest_schema)
|
|
validation_results.append((manifest_path.name, is_valid, message))
|
|
|
|
if is_valid:
|
|
print("✅")
|
|
else:
|
|
print("❌")
|
|
print(message)
|
|
|
|
# Validate merged response templates if provided
|
|
if args.merged_dir:
|
|
merged_dir = Path(args.merged_dir)
|
|
if not merged_dir.exists():
|
|
print(f"❌ Error: Merged templates directory not found: {merged_dir}")
|
|
sys.exit(1)
|
|
|
|
print(f"\n📋 Loading ResponseTemplateMerged schema from {schema_path}")
|
|
try:
|
|
merged_schema = load_openapi_schema(schema_path, 'ResponseTemplateMerged', debug=args.debug)
|
|
print(f"✅ ResponseTemplateMerged schema loaded successfully")
|
|
except Exception as e:
|
|
print(f"❌ Error loading merged schema: {e}")
|
|
sys.exit(1)
|
|
|
|
# Find all JSON files in merged directory (excluding manifest.json)
|
|
merged_files = [f for f in merged_dir.glob('*.json')
|
|
if f.name != 'manifest.json']
|
|
|
|
if merged_files:
|
|
print(f"\n🔍 Found {len(merged_files)} merged response template file(s) to validate\n")
|
|
|
|
for json_file in sorted(merged_files):
|
|
print(f"Validating {json_file.name}...", end=" ")
|
|
is_valid, message = validate_json_file(json_file, merged_schema)
|
|
validation_results.append((json_file.name, is_valid, message))
|
|
|
|
if is_valid:
|
|
print("✅")
|
|
else:
|
|
print("❌")
|
|
print(message)
|
|
else:
|
|
print(f"⚠️ No merged template JSON files found in {merged_dir}")
|
|
|
|
# Load ResponseTemplate schema
|
|
print(f"\n📋 Loading ResponseTemplate schema from {schema_path}")
|
|
try:
|
|
schema = load_openapi_schema(schema_path, 'ResponseTemplate', debug=args.debug)
|
|
print(f"✅ ResponseTemplate schema loaded successfully")
|
|
except Exception as e:
|
|
print(f"❌ Error loading schema: {e}")
|
|
sys.exit(1)
|
|
|
|
# Find all JSON files (excluding manifest if it's in the same directory)
|
|
json_files = [f for f in templates_dir.glob('*.json')
|
|
if not (args.manifest and f.name == Path(args.manifest).name)]
|
|
|
|
if not json_files:
|
|
if not args.manifest:
|
|
print(f"⚠️ No JSON files found in {templates_dir}")
|
|
sys.exit(0)
|
|
else:
|
|
print(f"\n🔍 Found {len(json_files)} response template file(s) to validate\n")
|
|
|
|
# Validate each file
|
|
for json_file in sorted(json_files):
|
|
print(f"Validating {json_file.name}...", end=" ")
|
|
is_valid, message = validate_json_file(json_file, schema)
|
|
validation_results.append((json_file.name, is_valid, message))
|
|
|
|
if is_valid:
|
|
print("✅")
|
|
else:
|
|
print("❌")
|
|
print(message)
|
|
|
|
# Summary
|
|
print("\n" + "="*60)
|
|
print("VALIDATION SUMMARY")
|
|
print("="*60)
|
|
|
|
passed = sum(1 for _, is_valid, _ in validation_results if is_valid)
|
|
failed = len(validation_results) - passed
|
|
|
|
for filename, is_valid, message in validation_results:
|
|
status = "✅ PASS" if is_valid else "❌ FAIL"
|
|
print(f"{status}: {filename}")
|
|
|
|
print(f"\nTotal: {len(validation_results)} | Passed: {passed} | Failed: {failed}")
|
|
|
|
if failed > 0:
|
|
print("\n❌ Validation failed!")
|
|
sys.exit(1)
|
|
else:
|
|
print("\n✅ All files validated successfully!")
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|