Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p

### Added

- CVSS4 Mappings, as a generative script which adds missing mappings for more recent VRT items.

### Removed

### Changed
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ Each mapping should be setup in the following structure:
└── ...

#### Supported Mappings
- [CVSS v4](mappings/cvss_v4/cvss_v4.json)
- [CVSS v3](mappings/cvss_v3/cvss_v3.json)
- [CWE](mappings/cwe/cwe.json)
- [Remediation Advice](mappings/remediation_advice/remediation_advice.json)
Expand Down
277 changes: 277 additions & 0 deletions lib/cvss_v4_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
#!/usr/bin/env python3
"""
Export VRT items with their CVSS v4 vectors and calculated scores.

This script outputs all VRT items along with their CVSS v4 mappings,
including calculated base scores for sense-checking.

Usage:
python cvss_v4_export.py [--format json|table] [--output filename]

Output includes:
- VRT ID (dot-separated path)
- VRT Name
- VRT Priority (P1-P5)
- CVSS v4 Vector
- CVSS v4 Base Score
- CVSS v4 Severity (None/Low/Medium/High/Critical)
"""

import argparse
import json
import os
import sys

# Add the lib directory to the path
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, script_dir)

# Change to repo root for file access
repo_root = os.path.dirname(script_dir)
os.chdir(repo_root)

from utils import utils

try:
from cvss import CVSS4
CVSS_AVAILABLE = True
except ImportError:
CVSS_AVAILABLE = False
print("Warning: cvss library not installed. Scores will not be calculated.", file=sys.stderr)
print("Install with: pip install cvss", file=sys.stderr)


def calculate_cvss4_score(vector_string):
"""
Calculate the CVSS v4 base score from a vector string.

Returns:
tuple: (score, severity) or (None, None) if calculation fails
"""
if not CVSS_AVAILABLE:
return None, None

try:
cvss = CVSS4(vector_string)
score = cvss.base_score
severity = cvss.severity
return score, severity
except Exception as e:
print(f"Warning: Could not calculate score for {vector_string}: {e}", file=sys.stderr)
return None, None


def priority_to_label(priority):
"""Convert numeric priority to P-label."""
if priority is None:
return None
return f"P{priority}"


def get_mapping_value(mapping_node, vrt_id_list, key, default=None):
"""
Recursively find a mapping value for a VRT ID path.

Walks up the tree to find the closest mapping value.
"""
if not vrt_id_list:
return mapping_node.get(key, default)

current_id = vrt_id_list[0]
remaining = vrt_id_list[1:]

if 'children' in mapping_node:
children = mapping_node['children']
# Convert to dict keyed by id for lookup
children_by_id = {c['id']: c for c in children}

if current_id in children_by_id:
child = children_by_id[current_id]
# Try to find value in child first
result = get_mapping_value(child, remaining, key, None)
if result is not None:
return result

# Fall back to current node's value
return mapping_node.get(key, default)


def flatten_vrt(vrt_content, cvss4_content, default_vector, prefix=None):
"""
Flatten the VRT and CVSS v4 mappings into a list of items.

Returns:
list of dicts with: vrt_id, name, priority, cvss_v4_vector, score, severity
"""
results = []

# Create mapping lookup
cvss4_by_id = {node['id']: node for node in cvss4_content}

def process_node(vrt_node, cvss4_node, id_path):
current_id = vrt_node['id']
current_path = id_path + [current_id]
vrt_id_str = '.'.join(current_path)

# Get CVSS v4 vector (from current node or inherited)
cvss4_vector = cvss4_node.get('cvss_v4') if cvss4_node else None
if cvss4_vector is None:
cvss4_vector = default_vector

# Calculate score
score, severity = calculate_cvss4_score(cvss4_vector)

# Only include leaf nodes (those with priority) or nodes with explicit vectors
is_leaf = 'priority' in vrt_node
has_explicit_vector = cvss4_node and 'cvss_v4' in cvss4_node

if is_leaf or has_explicit_vector:
results.append({
'vrt_id': vrt_id_str,
'name': vrt_node.get('name', ''),
'type': vrt_node.get('type', ''),
'priority': priority_to_label(vrt_node.get('priority')),
'cvss_v4_vector': cvss4_vector,
'cvss_v4_score': score,
'cvss_v4_severity': severity,
})

# Process children
if 'children' in vrt_node:
cvss4_children = {}
if cvss4_node and 'children' in cvss4_node:
cvss4_children = {c['id']: c for c in cvss4_node['children']}

for child in vrt_node['children']:
child_cvss4 = cvss4_children.get(child['id'])
# If child has no explicit vector, inherit from parent
if child_cvss4 is None:
child_cvss4 = {'cvss_v4': cvss4_vector} if has_explicit_vector else None
process_node(child, child_cvss4, current_path)

for vrt_node in vrt_content:
cvss4_node = cvss4_by_id.get(vrt_node['id'])
process_node(vrt_node, cvss4_node, [])

return results


def format_table(items):
"""Format items as a readable table."""
# Column widths
id_width = max(len(item['vrt_id']) for item in items)
name_width = min(50, max(len(item['name']) for item in items))

# Header
header = (
f"{'VRT ID':<{id_width}} | "
f"{'Name':<{name_width}} | "
f"{'Pri':>3} | "
f"{'Score':>5} | "
f"{'Severity':<8} | "
f"Vector"
)
separator = '-' * len(header)

lines = [header, separator]

for item in items:
name = item['name'][:name_width] if len(item['name']) > name_width else item['name']
score = f"{item['cvss_v4_score']:.1f}" if item['cvss_v4_score'] is not None else 'N/A'
severity = item['cvss_v4_severity'] or 'N/A'
priority = item['priority'] or 'N/A'

line = (
f"{item['vrt_id']:<{id_width}} | "
f"{name:<{name_width}} | "
f"{priority:>3} | "
f"{score:>5} | "
f"{severity:<8} | "
f"{item['cvss_v4_vector']}"
)
lines.append(line)

return '\n'.join(lines)


def main():
parser = argparse.ArgumentParser(
description='Export VRT items with CVSS v4 vectors and scores'
)
parser.add_argument(
'--format', '-f',
choices=['json', 'table'],
default='table',
help='Output format (default: table)'
)
parser.add_argument(
'--output', '-o',
help='Output file (default: stdout)'
)
parser.add_argument(
'--leaf-only',
action='store_true',
help='Only include leaf nodes (variants with priority)'
)

args = parser.parse_args()

# Load VRT
vrt = utils.get_json(utils.VRT_FILENAME)

# Load CVSS v4 mappings
cvss4_path = os.path.join(utils.MAPPING_DIR, utils.CVSS4_FILE)
cvss4 = utils.get_json(cvss4_path)

# Flatten and merge
items = flatten_vrt(
vrt['content'],
cvss4['content'],
cvss4['metadata']['default']
)

# Filter to leaf-only if requested
if args.leaf_only:
items = [item for item in items if item['priority'] is not None]

# Sort by VRT ID
items.sort(key=lambda x: x['vrt_id'])

# Format output
if args.format == 'json':
output = json.dumps(items, indent=2)
else:
output = format_table(items)

# Write output
if args.output:
with open(args.output, 'w') as f:
f.write(output)
print(f"Wrote {len(items)} items to {args.output}")
else:
print(output)

# Print summary statistics
if args.format == 'table':
print(f"\n--- Summary ---")
print(f"Total items: {len(items)}")

if CVSS_AVAILABLE:
scores = [item['cvss_v4_score'] for item in items if item['cvss_v4_score'] is not None]
if scores:
print(f"Score range: {min(scores):.1f} - {max(scores):.1f}")

# Severity distribution
severities = {}
for item in items:
sev = item['cvss_v4_severity'] or 'Unknown'
severities[sev] = severities.get(sev, 0) + 1

print("Severity distribution:")
for sev in ['Critical', 'High', 'Medium', 'Low', 'None', 'Unknown']:
if sev in severities:
print(f" {sev}: {severities[sev]}")


if __name__ == '__main__':
main()
Loading