Files
pdf-reporting/report_generator/severity.py
T
alexanderandClaude Opus 5.5 8f47303605 Grade SARIF findings by score, then CWE, then level
A result's own security-severity score is banded like CVSS; without one,
the finding is graded by a hand-built CWE table; failing that, by its
SARIF level, one band lower than before. The scanners' levels said more
about the tool than about the risk: a container running as root came out
Critical and a hard-coded password Medium.

The summariser only reads security-severity off the rule, and Lynkeus
scores each result, so the runs are summarised here to keep that score.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:06:44 +02:00

130 lines
5.8 KiB
Python

"""The severity a SARIF finding is reported under.
In order of preference:
1. The finding's own ``security-severity`` score, banded like CVSS. Where a tool scored the finding
itself -- Lynkeus does -- that is the most specific reading there is.
2. Its weakness class, from :data:`CWE_SEVERITY`. The tools do not agree on what their levels mean
-- Trivy's ``error`` for a container running as root and Bandit's ``note`` for a hard-coded
database password say more about the tool than about the risk -- so the CWE is preferred to them.
3. Its SARIF level, from :data:`SEVERITY_OF_LEVEL`, for a finding with neither.
Each entry of the table is the severity a confirmed instance of that weakness typically carries, before any
context about the application is known: roughly where the CVEs filed under it land on the CVSS
bands. Critical is kept for classes that usually hand an attacker code execution or the whole data
store; weaknesses that only matter together with another flaw, or that harden rather than defend,
sit at Medium or Low.
"""
from __future__ import annotations
import re
CWE_SEVERITY = {
# Injection that typically ends in code execution or full database access.
"CWE-77": "Critical", # Command injection
"CWE-78": "Critical", # OS command injection
"CWE-89": "Critical", # SQL injection
"CWE-94": "Critical", # Code injection
"CWE-95": "Critical", # Eval injection
"CWE-502": "Critical", # Deserialization of untrusted data
# Direct compromise of confidentiality or access control, usually needing some foothold.
"CWE-22": "High", # Path traversal
"CWE-96": "High", # Static code injection
"CWE-259": "High", # Hard-coded password
"CWE-287": "High", # Improper authentication
"CWE-306": "High", # Missing authentication for a critical function
"CWE-321": "High", # Hard-coded cryptographic key
"CWE-434": "High", # Unrestricted upload of dangerous file type
"CWE-611": "High", # XML external entities
"CWE-798": "High", # Hard-coded credentials
"CWE-862": "High", # Missing authorization
"CWE-863": "High", # Incorrect authorization
"CWE-918": "High", # Server-side request forgery
# Real weaknesses whose impact depends on context, or that need another flaw to be exploited.
"CWE-15": "Medium", # External control of a configuration setting
"CWE-20": "Medium", # Improper input validation
"CWE-79": "Medium", # Cross-site scripting
"CWE-200": "Medium", # Exposure of sensitive information
"CWE-250": "Medium", # Execution with unnecessary privileges
"CWE-269": "Medium", # Improper privilege management
"CWE-295": "Medium", # Improper certificate validation
"CWE-312": "Medium", # Cleartext storage of sensitive information
"CWE-319": "Medium", # Cleartext transmission of sensitive information
"CWE-326": "Medium", # Inadequate encryption strength
"CWE-327": "Medium", # Broken or risky cryptographic algorithm
"CWE-328": "Medium", # Weak hash
"CWE-330": "Medium", # Insufficiently random values
"CWE-338": "Medium", # Cryptographically weak PRNG
"CWE-352": "Medium", # Cross-site request forgery
"CWE-400": "Medium", # Uncontrolled resource consumption
"CWE-522": "Medium", # Insufficiently protected credentials
"CWE-532": "Medium", # Sensitive information in a log file
"CWE-601": "Medium", # Open redirect
"CWE-732": "Medium", # Incorrect permission assignment
"CWE-770": "Medium", # Allocation of resources without limits
"CWE-1333": "Medium", # Inefficient regular expression (ReDoS)
# Hygiene: worth fixing, rarely exploitable on its own.
"CWE-117": "Low", # Log injection
"CWE-209": "Low", # Information in an error message
"CWE-377": "Low", # Insecure temporary file
"CWE-390": "Low", # Error detected without action
"CWE-391": "Low", # Unchecked error condition
"CWE-614": "Low", # Sensitive cookie without the Secure flag
"CWE-703": "Low", # Improper handling of exceptional conditions
"CWE-754": "Low", # Improper check for unusual conditions
"CWE-755": "Low", # Improper handling of exceptional conditions
"CWE-1004": "Low", # Sensitive cookie without the HttpOnly flag
}
# For a finding whose CWE is not in the table, or that has none. One band below the old one-to-one
# reading of the level, because a tool's "error" is not a statement that the finding is critical.
SEVERITY_OF_LEVEL = {
"error": "High",
"warning": "Medium",
"note": "Low",
"none": "Low",
"info": "Low",
}
# The CVSS v3 qualitative bands, by their lower bounds. A score of 0 has no band of its own in the
# report and reads as Low.
SCORE_BANDS = ((9.0, "Critical"), (7.0, "High"), (4.0, "Medium"))
_CWE = re.compile(r"CWE-(\d+)", re.IGNORECASE)
def finding_severity(rule_id: str, level: str, security_severity: str = "") -> str:
"""The severity a SARIF finding is reported under.
Args:
rule_id: The finding's rule. The pipeline's tools name their rules by CWE.
level: The SARIF level.
security_severity: The finding's ``security-severity`` score, empty where it has none.
Returns:
One of Critical, High, Medium or Low.
"""
scored = _score_band(security_severity)
if scored is not None:
return scored
found = _CWE.search(rule_id)
known = CWE_SEVERITY.get(f"CWE-{found.group(1)}") if found else None
return known or SEVERITY_OF_LEVEL.get(level, "Low")
def _score_band(stated: str) -> str | None:
"""The band a ``security-severity`` score falls in, or nothing where it is not a score."""
try:
score = float(stated)
except ValueError:
return None
if not 0 <= score <= 10:
return None
return next((name for bound, name in SCORE_BANDS if score >= bound), "Low")