Updates to new format #1

Open
fklement wants to merge 17 commits from CipherCell/pdf-reporting:updates-to-new-format into main
pull from: CipherCell/pdf-reporting:updates-to-new-format
2 changed files with 155 additions and 14 deletions
Showing only changes of commit 8f47303605 - Show all commits

No files matched your search

+26 -14
View File
@@ -22,10 +22,11 @@ from collections import Counter
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any
from .vendor.cipherverifier.core.services.sarif import summarize_sarif
from .vendor.cipherverifier.core.services.sarif import _build_finding, _build_report, _rules_by_id
from .vendor.cipherverifier.core.services.vdr import summarize_vdr
from .layout import TITLE
from .severity import finding_severity
if TYPE_CHECKING:
from .vendor.cipherverifier.core.services.sarif import Finding, SarifReport
@@ -40,16 +41,6 @@ UNKNOWN = "<xxx>"
MOST_CRITICAL = 6
BARS = 10
# The severity a SARIF level is reported under. SARIF names four levels and the report bands five,
# so the mapping is written down rather than inferred from the spelling.
SEVERITY_OF_LEVEL = {
"error": "Critical",
"warning": "High",
"note": "Medium",
"none": "Low",
"info": "Low",
}
SEVERITY_ORDER = ("Critical", "High", "Medium", "Low")
# A full git commit SHA.
@@ -118,7 +109,28 @@ def _tools(sarif: Any) -> list[tuple[str, SarifReport]]: # noqa: ANN401
runs = [run for run in sarif.get("runs") or [] if isinstance(run, dict)]
return [(_tool_name(run), summarize_sarif({"runs": [run]})) for run in runs]
return [(_tool_name(run), _summarize(run)) for run in runs]
def _summarize(run: dict[str, Any]) -> SarifReport:
"""Summarise one run, as :func:`summarize_sarif` would, keeping each result's own score.
The summariser reads ``security-severity`` off the rule only. Lynkeus scores each result instead,
and a result's own score is the more specific one, so it takes the rule's place.
"""
rules = _rules_by_id([run])
findings = []
for result in run.get("results") or []:
finding = _build_finding(result, run, rules)
stated = (result.get("properties") or {}).get("security-severity")
if stated is not None:
finding.rule_security_severity = str(stated)
findings.append(finding)
return _build_report(findings, ranked=any(finding.rank is not None for finding in findings))
def _tool_name(run: dict[str, Any]) -> str:
@@ -732,8 +744,8 @@ def _anchor(title: str) -> str:
def _finding_severity(finding: Finding) -> str:
"""The severity band a SARIF level falls in."""
return SEVERITY_OF_LEVEL.get(finding.level, "Low")
"""The severity band a SARIF finding falls in; see :mod:`severity`."""
return finding_severity(finding.rule_id, finding.level, finding.rule_security_severity)
def _vdr_severity(vulnerability: Vulnerability) -> str:
+129
View File
@@ -0,0 +1,129 @@
"""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")