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>
130 lines
5.8 KiB
Python
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")
|