chore(revision): resolve comments

This commit is contained in:
pedrooot
2025-12-18 12:33:24 +01:00
parent 489454b5c6
commit 8cde5a1636
7 changed files with 126 additions and 142 deletions

View File

@@ -4,6 +4,7 @@ from .base import (
ComplianceData, ComplianceData,
RequirementData, RequirementData,
create_pdf_styles, create_pdf_styles,
get_requirement_metadata,
) )
# Chart functions # Chart functions
@@ -99,6 +100,7 @@ __all__ = [
"ComplianceData", "ComplianceData",
"RequirementData", "RequirementData",
"create_pdf_styles", "create_pdf_styles",
"get_requirement_metadata",
# Framework-specific generators # Framework-specific generators
"ThreatScoreReportGenerator", "ThreatScoreReportGenerator",
"ENSReportGenerator", "ENSReportGenerator",

View File

@@ -13,13 +13,25 @@ from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas
from reportlab.platypus import Image, PageBreak, Paragraph, SimpleDocTemplate, Spacer from reportlab.platypus import Image, PageBreak, Paragraph, SimpleDocTemplate, Spacer
from tasks.jobs.threatscore_utils import (
_aggregate_requirement_statistics_from_database,
_calculate_requirements_data_from_statistics,
_load_findings_for_requirement_checks,
)
from api.db_router import READ_REPLICA_ALIAS from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction from api.db_utils import rls_transaction
from api.models import Provider, StatusChoices from api.models import Provider, StatusChoices
from api.utils import initialize_prowler_provider
from prowler.lib.check.compliance_models import Compliance from prowler.lib.check.compliance_models import Compliance
from prowler.lib.outputs.finding import Finding as FindingOutput from prowler.lib.outputs.finding import Finding as FindingOutput
from .components import (
ColumnConfig,
create_data_table,
create_info_table,
create_status_badge,
)
from .config import ( from .config import (
COLOR_BG_BLUE, COLOR_BG_BLUE,
COLOR_BG_LIGHT_BLUE, COLOR_BG_LIGHT_BLUE,
@@ -37,13 +49,17 @@ from .config import (
logger = get_task_logger(__name__) logger = get_task_logger(__name__)
# Register fonts (done once at module load) # Register fonts (done once at module load)
_FONTS_REGISTERED = False _fonts_registered: bool = False
def _register_fonts() -> None: def _register_fonts() -> None:
"""Register custom fonts for PDF generation.""" """Register custom fonts for PDF generation.
global _FONTS_REGISTERED
if _FONTS_REGISTERED: Uses a module-level flag to ensure fonts are only registered once,
avoiding duplicate registration errors from reportlab.
"""
global _fonts_registered
if _fonts_registered:
return return
fonts_dir = os.path.join(os.path.dirname(__file__), "../../assets/fonts") fonts_dir = os.path.join(os.path.dirname(__file__), "../../assets/fonts")
@@ -62,7 +78,7 @@ def _register_fonts() -> None:
) )
) )
_FONTS_REGISTERED = True _fonts_registered = True
# ============================================================================= # =============================================================================
@@ -133,6 +149,35 @@ class ComplianceData:
prowler_provider: Any = None prowler_provider: Any = None
def get_requirement_metadata(
requirement_id: str,
attributes_by_requirement_id: dict[str, dict],
) -> Any | None:
"""Get the first requirement metadata object from attributes.
This helper function extracts the requirement metadata (req_attributes)
from the attributes dictionary. It's a common pattern used across all
report generators.
Args:
requirement_id: The requirement ID to look up.
attributes_by_requirement_id: Mapping of requirement IDs to their attributes.
Returns:
The first requirement attribute object, or None if not found.
Example:
>>> meta = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
>>> if meta:
... section = getattr(meta, "Section", "Unknown")
"""
req_attrs = attributes_by_requirement_id.get(requirement_id, {})
meta_list = req_attrs.get("attributes", {}).get("req_attributes", [])
if meta_list:
return meta_list[0]
return None
# ============================================================================= # =============================================================================
# PDF Styles Cache # PDF Styles Cache
# ============================================================================= # =============================================================================
@@ -435,8 +480,6 @@ class BaseComplianceReportGenerator(ABC):
Returns: Returns:
List of ReportLab elements List of ReportLab elements
""" """
from .components import create_info_table
elements = [] elements = []
# Prowler logo # Prowler logo
@@ -493,17 +536,24 @@ class BaseComplianceReportGenerator(ABC):
Returns: Returns:
List of ReportLab elements List of ReportLab elements
""" """
from tasks.jobs.threatscore_utils import _load_findings_for_requirement_checks
from .components import create_status_badge
elements = [] elements = []
only_failed = kwargs.get("only_failed", True) only_failed = kwargs.get("only_failed", True)
include_manual = kwargs.get("include_manual", False)
# Filter requirements if needed # Filter requirements if needed
requirements = data.requirements requirements = data.requirements
if only_failed: if only_failed:
requirements = [r for r in requirements if r.status == StatusChoices.FAIL] # Include FAIL requirements, and optionally MANUAL if include_manual is True
if include_manual:
requirements = [
r
for r in requirements
if r.status in (StatusChoices.FAIL, StatusChoices.MANUAL)
]
else:
requirements = [
r for r in requirements if r.status == StatusChoices.FAIL
]
# Collect all check IDs for requirements that will be displayed # Collect all check IDs for requirements that will be displayed
# This allows us to load only the findings we actually need (memory optimization) # This allows us to load only the findings we actually need (memory optimization)
@@ -602,13 +652,6 @@ class BaseComplianceReportGenerator(ABC):
Returns: Returns:
Aggregated ComplianceData object Aggregated ComplianceData object
""" """
from tasks.jobs.threatscore_utils import (
_aggregate_requirement_statistics_from_database,
_calculate_requirements_data_from_statistics,
)
from api.utils import initialize_prowler_provider
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS): with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
# Load provider # Load provider
if provider_obj is None: if provider_obj is None:
@@ -672,7 +715,7 @@ class BaseComplianceReportGenerator(ABC):
description=description, description=description,
requirements=requirements, requirements=requirements,
attributes_by_requirement_id=attributes_by_requirement_id, attributes_by_requirement_id=attributes_by_requirement_id,
findings_by_check_id=findings_cache or {}, findings_by_check_id=findings_cache if findings_cache is not None else {},
provider_obj=provider_obj, provider_obj=provider_obj,
prowler_provider=prowler_provider, prowler_provider=prowler_provider,
) )
@@ -744,7 +787,6 @@ class BaseComplianceReportGenerator(ABC):
Returns: Returns:
ReportLab Table element ReportLab Table element
""" """
from .components import ColumnConfig, create_data_table
def get_finding_title(f): def get_finding_title(f):
metadata = getattr(f, "metadata", None) metadata = getattr(f, "metadata", None)

View File

@@ -8,7 +8,11 @@ from reportlab.platypus import Image, PageBreak, Paragraph, Spacer, Table, Table
from api.models import StatusChoices from api.models import StatusChoices
from .base import BaseComplianceReportGenerator, ComplianceData from .base import (
BaseComplianceReportGenerator,
ComplianceData,
get_requirement_metadata,
)
from .charts import create_horizontal_bar_chart, create_radar_chart from .charts import create_horizontal_bar_chart, create_radar_chart
from .components import get_color_for_compliance from .components import get_color_for_compliance
from .config import ( from .config import (
@@ -330,10 +334,8 @@ class ENSReportGenerator(BaseComplianceReportGenerator):
if req.status == StatusChoices.MANUAL: if req.status == StatusChoices.MANUAL:
continue continue
req_attrs = data.attributes_by_requirement_id.get(req.id, {}) m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
meta = req_attrs.get("attributes", {}).get("req_attributes", [{}]) if m:
if meta:
m = meta[0]
marco = getattr(m, "Marco", "Otros") marco = getattr(m, "Marco", "Otros")
categoria = getattr(m, "Categoria", "Sin categoría") categoria = getattr(m, "Categoria", "Sin categoría")
descripcion = getattr(m, "DescripcionControl", req.description) descripcion = getattr(m, "DescripcionControl", req.description)
@@ -442,10 +444,8 @@ class ENSReportGenerator(BaseComplianceReportGenerator):
if req.status == StatusChoices.MANUAL: if req.status == StatusChoices.MANUAL:
continue continue
req_attrs = data.attributes_by_requirement_id.get(req.id, {}) m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
meta = req_attrs.get("attributes", {}).get("req_attributes", [{}]) if m:
if meta:
m = meta[0]
nivel = getattr(m, "Nivel", "").lower() nivel = getattr(m, "Nivel", "").lower()
nivel_data[nivel]["total"] += 1 nivel_data[nivel]["total"] += 1
if req.status == StatusChoices.PASS: if req.status == StatusChoices.PASS:
@@ -520,10 +520,8 @@ class ENSReportGenerator(BaseComplianceReportGenerator):
if req.status == StatusChoices.MANUAL: if req.status == StatusChoices.MANUAL:
continue continue
req_attrs = data.attributes_by_requirement_id.get(req.id, {}) m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
meta = req_attrs.get("attributes", {}).get("req_attributes", [{}]) if m:
if meta:
m = meta[0]
marco = getattr(m, "Marco", "otros") marco = getattr(m, "Marco", "otros")
categoria = getattr(m, "Categoria", "sin categoría") categoria = getattr(m, "Categoria", "sin categoría")
# Combined key: "marco - categoría" # Combined key: "marco - categoría"
@@ -554,10 +552,8 @@ class ENSReportGenerator(BaseComplianceReportGenerator):
if req.status == StatusChoices.MANUAL: if req.status == StatusChoices.MANUAL:
continue continue
req_attrs = data.attributes_by_requirement_id.get(req.id, {}) m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
meta = req_attrs.get("attributes", {}).get("req_attributes", [{}]) if m:
if meta:
m = meta[0]
dimensiones = getattr(m, "Dimensiones", []) dimensiones = getattr(m, "Dimensiones", [])
if isinstance(dimensiones, str): if isinstance(dimensiones, str):
dimensiones = [d.strip().lower() for d in dimensiones.split(",")] dimensiones = [d.strip().lower() for d in dimensiones.split(",")]
@@ -600,10 +596,8 @@ class ENSReportGenerator(BaseComplianceReportGenerator):
if req.status == StatusChoices.MANUAL: if req.status == StatusChoices.MANUAL:
continue continue
req_attrs = data.attributes_by_requirement_id.get(req.id, {}) m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
meta = req_attrs.get("attributes", {}).get("req_attributes", [{}]) if m:
if meta:
m = meta[0]
tipo = getattr(m, "Tipo", "").lower() tipo = getattr(m, "Tipo", "").lower()
tipo_data[tipo]["total"] += 1 tipo_data[tipo]["total"] += 1
if req.status == StatusChoices.PASS: if req.status == StatusChoices.PASS:
@@ -661,10 +655,8 @@ class ENSReportGenerator(BaseComplianceReportGenerator):
if req.status != StatusChoices.FAIL: if req.status != StatusChoices.FAIL:
continue continue
req_attrs = data.attributes_by_requirement_id.get(req.id, {}) m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
meta = req_attrs.get("attributes", {}).get("req_attributes", [{}]) if m:
if meta:
m = meta[0]
nivel = getattr(m, "Nivel", "").lower() nivel = getattr(m, "Nivel", "").lower()
if nivel == "alto": if nivel == "alto":
critical_failed.append( critical_failed.append(
@@ -766,14 +758,22 @@ class ENSReportGenerator(BaseComplianceReportGenerator):
List of ReportLab elements. List of ReportLab elements.
""" """
elements = [] elements = []
include_manual = kwargs.get("include_manual", True)
elements.append(Paragraph("Detalle de Requisitos", self.styles["h1"])) elements.append(Paragraph("Detalle de Requisitos", self.styles["h1"]))
elements.append(Spacer(1, 0.2 * inch)) elements.append(Spacer(1, 0.2 * inch))
# Get failed requirements (non-manual) # Get failed requirements, and optionally manual requirements
failed_requirements = [ if include_manual:
r for r in data.requirements if r.status == StatusChoices.FAIL failed_requirements = [
] r
for r in data.requirements
if r.status in (StatusChoices.FAIL, StatusChoices.MANUAL)
]
else:
failed_requirements = [
r for r in data.requirements if r.status == StatusChoices.FAIL
]
if not failed_requirements: if not failed_requirements:
elements.append( elements.append(
@@ -802,13 +802,11 @@ class ENSReportGenerator(BaseComplianceReportGenerator):
} }
for req in failed_requirements: for req in failed_requirements:
req_attrs = data.attributes_by_requirement_id.get(req.id, {}) m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
meta = req_attrs.get("attributes", {}).get("req_attributes", [{}])
if not meta: if not m:
continue continue
m = meta[0]
nivel = getattr(m, "Nivel", "").lower() nivel = getattr(m, "Nivel", "").lower()
tipo = getattr(m, "Tipo", "") tipo = getattr(m, "Tipo", "")
modo = getattr(m, "ModoEjecucion", "") modo = getattr(m, "ModoEjecucion", "")

View File

@@ -6,7 +6,11 @@ from reportlab.platypus import Image, PageBreak, Paragraph, Spacer, Table, Table
from api.models import StatusChoices from api.models import StatusChoices
from .base import BaseComplianceReportGenerator, ComplianceData from .base import (
BaseComplianceReportGenerator,
ComplianceData,
get_requirement_metadata,
)
from .charts import create_horizontal_bar_chart, get_chart_color_for_percentage from .charts import create_horizontal_bar_chart, get_chart_color_for_percentage
from .config import ( from .config import (
COLOR_BORDER_GRAY, COLOR_BORDER_GRAY,
@@ -263,10 +267,8 @@ class NIS2ReportGenerator(BaseComplianceReportGenerator):
# Organize by section number and subsection # Organize by section number and subsection
sections = {} sections = {}
for req in data.requirements: for req in data.requirements:
req_attrs = data.attributes_by_requirement_id.get(req.id, {}) m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
meta = req_attrs.get("attributes", {}).get("req_attributes", [{}]) if m:
if meta:
m = meta[0]
full_section = getattr(m, "Section", "Other") full_section = getattr(m, "Section", "Other")
# Extract section number from full title (e.g., "1 POLICY..." -> "1") # Extract section number from full title (e.g., "1 POLICY..." -> "1")
section_num = _extract_section_number(full_section) section_num = _extract_section_number(full_section)
@@ -343,10 +345,8 @@ class NIS2ReportGenerator(BaseComplianceReportGenerator):
if req.status == StatusChoices.MANUAL: if req.status == StatusChoices.MANUAL:
continue continue
req_attrs = data.attributes_by_requirement_id.get(req.id, {}) m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
meta = req_attrs.get("attributes", {}).get("req_attributes", [{}]) if m:
if meta:
m = meta[0]
full_section = getattr(m, "Section", "Other") full_section = getattr(m, "Section", "Other")
# Extract section number from full title (e.g., "1 POLICY..." -> "1") # Extract section number from full title (e.g., "1 POLICY..." -> "1")
section_num = _extract_section_number(full_section) section_num = _extract_section_number(full_section)
@@ -385,10 +385,8 @@ class NIS2ReportGenerator(BaseComplianceReportGenerator):
subsection_scores = defaultdict(lambda: {"passed": 0, "failed": 0, "manual": 0}) subsection_scores = defaultdict(lambda: {"passed": 0, "failed": 0, "manual": 0})
for req in data.requirements: for req in data.requirements:
req_attrs = data.attributes_by_requirement_id.get(req.id, {}) m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
meta = req_attrs.get("attributes", {}).get("req_attributes", [{}]) if m:
if meta:
m = meta[0]
full_section = getattr(m, "Section", "") full_section = getattr(m, "Section", "")
subsection = getattr(m, "SubSection", "") subsection = getattr(m, "SubSection", "")
# Use section number + subsection for grouping # Use section number + subsection for grouping

View File

@@ -4,7 +4,11 @@ from reportlab.platypus import Image, PageBreak, Paragraph, Spacer, Table, Table
from api.models import StatusChoices from api.models import StatusChoices
from .base import BaseComplianceReportGenerator, ComplianceData from .base import (
BaseComplianceReportGenerator,
ComplianceData,
get_requirement_metadata,
)
from .charts import create_vertical_bar_chart, get_chart_color_for_percentage from .charts import create_vertical_bar_chart, get_chart_color_for_percentage
from .components import get_color_for_compliance, get_color_for_weight from .components import get_color_for_compliance, get_color_for_weight
from .config import COLOR_HIGH_RISK, COLOR_WHITE from .config import COLOR_HIGH_RISK, COLOR_WHITE
@@ -145,10 +149,9 @@ class ThreatScoreReportGenerator(BaseComplianceReportGenerator):
# Organize requirements by section and subsection # Organize requirements by section and subsection
sections = {} sections = {}
for req_id, req_attrs in data.attributes_by_requirement_id.items(): for req_id in data.attributes_by_requirement_id:
meta = req_attrs.get("attributes", {}).get("req_attributes", [{}]) m = get_requirement_metadata(req_id, data.attributes_by_requirement_id)
if meta: if m:
m = meta[0]
section = getattr(m, "Section", "N/A") section = getattr(m, "Section", "N/A")
subsection = getattr(m, "SubSection", "N/A") subsection = getattr(m, "SubSection", "N/A")
title = getattr(m, "Title", "N/A") title = getattr(m, "Title", "N/A")
@@ -202,10 +205,8 @@ class ThreatScoreReportGenerator(BaseComplianceReportGenerator):
sections_data = {} sections_data = {}
for req in data.requirements: for req in data.requirements:
req_attrs = data.attributes_by_requirement_id.get(req.id, {}) m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
meta = req_attrs.get("attributes", {}).get("req_attributes", [{}]) if m:
if meta:
m = meta[0]
section = getattr(m, "Section", "Other") section = getattr(m, "Section", "Other")
all_sections.add(section) all_sections.add(section)
@@ -285,11 +286,9 @@ class ThreatScoreReportGenerator(BaseComplianceReportGenerator):
continue continue
has_findings = True has_findings = True
req_attrs = data.attributes_by_requirement_id.get(req.id, {}) m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
meta = req_attrs.get("attributes", {}).get("req_attributes", [{}])
if meta: if m:
m = meta[0]
risk_level_raw = getattr(m, "LevelOfRisk", 0) risk_level_raw = getattr(m, "LevelOfRisk", 0)
weight_raw = getattr(m, "Weight", 0) weight_raw = getattr(m, "Weight", 0)
# Ensure numeric types for calculations (compliance data may have str) # Ensure numeric types for calculations (compliance data may have str)
@@ -333,11 +332,9 @@ class ThreatScoreReportGenerator(BaseComplianceReportGenerator):
if req.status != StatusChoices.FAIL: if req.status != StatusChoices.FAIL:
continue continue
req_attrs = data.attributes_by_requirement_id.get(req.id, {}) m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
meta = req_attrs.get("attributes", {}).get("req_attributes", [{}])
if meta: if m:
m = meta[0]
risk_level_raw = getattr(m, "LevelOfRisk", 0) risk_level_raw = getattr(m, "LevelOfRisk", 0)
weight_raw = getattr(m, "Weight", 0) weight_raw = getattr(m, "Weight", 0)
# Ensure numeric types for calculations (compliance data may have str) # Ensure numeric types for calculations (compliance data may have str)

View File

@@ -2,6 +2,7 @@ import io
import pytest import pytest
from reportlab.lib.units import inch from reportlab.lib.units import inch
from reportlab.platypus import Image, LongTable, Paragraph, Spacer, Table
from tasks.jobs.reports import ( # Configuration; Colors; Components; Charts; Base from tasks.jobs.reports import ( # Configuration; Colors; Components; Charts; Base
CHART_COLOR_GREEN_1, CHART_COLOR_GREEN_1,
CHART_COLOR_GREEN_2, CHART_COLOR_GREEN_2,
@@ -9,7 +10,10 @@ from tasks.jobs.reports import ( # Configuration; Colors; Components; Charts; B
CHART_COLOR_RED, CHART_COLOR_RED,
CHART_COLOR_YELLOW, CHART_COLOR_YELLOW,
COLOR_BLUE, COLOR_BLUE,
COLOR_DARK_GRAY,
COLOR_HIGH_RISK, COLOR_HIGH_RISK,
COLOR_LOW_RISK,
COLOR_MEDIUM_RISK,
COLOR_SAFE, COLOR_SAFE,
FRAMEWORK_REGISTRY, FRAMEWORK_REGISTRY,
BaseComplianceReportGenerator, BaseComplianceReportGenerator,
@@ -155,14 +159,10 @@ class TestColorHelpers:
def test_get_color_for_risk_level_medium(self): def test_get_color_for_risk_level_medium(self):
"""Test medium risk level returns orange.""" """Test medium risk level returns orange."""
from tasks.jobs.reports import COLOR_MEDIUM_RISK
assert get_color_for_risk_level(3) == COLOR_MEDIUM_RISK assert get_color_for_risk_level(3) == COLOR_MEDIUM_RISK
def test_get_color_for_risk_level_low(self): def test_get_color_for_risk_level_low(self):
"""Test low risk level returns yellow.""" """Test low risk level returns yellow."""
from tasks.jobs.reports import COLOR_LOW_RISK
assert get_color_for_risk_level(2) == COLOR_LOW_RISK assert get_color_for_risk_level(2) == COLOR_LOW_RISK
def test_get_color_for_risk_level_safe(self): def test_get_color_for_risk_level_safe(self):
@@ -181,8 +181,6 @@ class TestColorHelpers:
def test_get_color_for_weight_medium(self): def test_get_color_for_weight_medium(self):
"""Test medium weight returns yellow.""" """Test medium weight returns yellow."""
from tasks.jobs.reports import COLOR_LOW_RISK
assert get_color_for_weight(100) == COLOR_LOW_RISK assert get_color_for_weight(100) == COLOR_LOW_RISK
assert get_color_for_weight(51) == COLOR_LOW_RISK assert get_color_for_weight(51) == COLOR_LOW_RISK
@@ -198,8 +196,6 @@ class TestColorHelpers:
def test_get_color_for_compliance_medium(self): def test_get_color_for_compliance_medium(self):
"""Test medium compliance returns yellow.""" """Test medium compliance returns yellow."""
from tasks.jobs.reports import COLOR_LOW_RISK
assert get_color_for_compliance(79) == COLOR_LOW_RISK assert get_color_for_compliance(79) == COLOR_LOW_RISK
assert get_color_for_compliance(60) == COLOR_LOW_RISK assert get_color_for_compliance(60) == COLOR_LOW_RISK
@@ -220,8 +216,6 @@ class TestColorHelpers:
def test_get_status_color_manual(self): def test_get_status_color_manual(self):
"""Test MANUAL status returns gray.""" """Test MANUAL status returns gray."""
from tasks.jobs.reports import COLOR_DARK_GRAY
assert get_status_color("MANUAL") == COLOR_DARK_GRAY assert get_status_color("MANUAL") == COLOR_DARK_GRAY
@@ -235,8 +229,6 @@ class TestChartColorHelpers:
def test_chart_color_for_medium_high_percentage(self): def test_chart_color_for_medium_high_percentage(self):
"""Test medium-high percentage returns light green.""" """Test medium-high percentage returns light green."""
from tasks.jobs.reports import CHART_COLOR_GREEN_2
assert get_chart_color_for_percentage(79) == CHART_COLOR_GREEN_2 assert get_chart_color_for_percentage(79) == CHART_COLOR_GREEN_2
assert get_chart_color_for_percentage(60) == CHART_COLOR_GREEN_2 assert get_chart_color_for_percentage(60) == CHART_COLOR_GREEN_2
@@ -274,8 +266,6 @@ class TestBadgeComponents:
def test_create_badge_returns_table(self): def test_create_badge_returns_table(self):
"""Test create_badge returns a Table object.""" """Test create_badge returns a Table object."""
from reportlab.platypus import Table
badge = create_badge("Test", COLOR_BLUE) badge = create_badge("Test", COLOR_BLUE)
assert isinstance(badge, Table) assert isinstance(badge, Table)
@@ -286,8 +276,6 @@ class TestBadgeComponents:
def test_create_status_badge_pass(self): def test_create_status_badge_pass(self):
"""Test status badge for PASS.""" """Test status badge for PASS."""
from reportlab.platypus import Table
badge = create_status_badge("PASS") badge = create_status_badge("PASS")
assert isinstance(badge, Table) assert isinstance(badge, Table)
@@ -298,8 +286,6 @@ class TestBadgeComponents:
def test_create_multi_badge_row_with_badges(self): def test_create_multi_badge_row_with_badges(self):
"""Test multi-badge row with data.""" """Test multi-badge row with data."""
from reportlab.platypus import Table
badges = [ badges = [
("A", COLOR_BLUE), ("A", COLOR_BLUE),
("B", COLOR_SAFE), ("B", COLOR_SAFE),
@@ -318,8 +304,6 @@ class TestRiskComponent:
def test_create_risk_component_returns_table(self): def test_create_risk_component_returns_table(self):
"""Test risk component returns a Table.""" """Test risk component returns a Table."""
from reportlab.platypus import Table
component = create_risk_component(risk_level=4, weight=100, score=50) component = create_risk_component(risk_level=4, weight=100, score=50)
assert isinstance(component, Table) assert isinstance(component, Table)
@@ -339,8 +323,6 @@ class TestTableComponents:
def test_create_info_table(self): def test_create_info_table(self):
"""Test info table creation.""" """Test info table creation."""
from reportlab.platypus import Table
rows = [ rows = [
("Label 1:", "Value 1"), ("Label 1:", "Value 1"),
("Label 2:", "Value 2"), ("Label 2:", "Value 2"),
@@ -356,8 +338,6 @@ class TestTableComponents:
def test_create_data_table(self): def test_create_data_table(self):
"""Test data table creation.""" """Test data table creation."""
from reportlab.platypus import Table
data = [ data = [
{"name": "Item 1", "value": "100"}, {"name": "Item 1", "value": "100"},
{"name": "Item 2", "value": "200"}, {"name": "Item 2", "value": "200"},
@@ -380,8 +360,6 @@ class TestTableComponents:
def test_create_summary_table(self): def test_create_summary_table(self):
"""Test summary table creation.""" """Test summary table creation."""
from reportlab.platypus import Table
table = create_summary_table( table = create_summary_table(
label="Score:", label="Score:",
value="85%", value="85%",
@@ -391,8 +369,6 @@ class TestTableComponents:
def test_create_summary_table_with_custom_widths(self): def test_create_summary_table_with_custom_widths(self):
"""Test summary table with custom widths.""" """Test summary table with custom widths."""
from reportlab.platypus import Table
table = create_summary_table( table = create_summary_table(
label="ThreatScore:", label="ThreatScore:",
value="92.5%", value="92.5%",
@@ -408,8 +384,6 @@ class TestFindingsTable:
def test_create_findings_table_with_dicts(self): def test_create_findings_table_with_dicts(self):
"""Test findings table creation with dict data.""" """Test findings table creation with dict data."""
from reportlab.platypus import Table
findings = [ findings = [
{ {
"title": "Finding 1", "title": "Finding 1",
@@ -450,8 +424,6 @@ class TestSectionHeader:
def test_create_section_header_with_spacer(self): def test_create_section_header_with_spacer(self):
"""Test section header with spacer.""" """Test section header with spacer."""
from reportlab.platypus import Paragraph, Spacer
styles = create_pdf_styles() styles = create_pdf_styles()
elements = create_section_header("Test Header", styles["h1"]) elements = create_section_header("Test Header", styles["h1"])
@@ -461,8 +433,6 @@ class TestSectionHeader:
def test_create_section_header_without_spacer(self): def test_create_section_header_without_spacer(self):
"""Test section header without spacer.""" """Test section header without spacer."""
from reportlab.platypus import Paragraph
styles = create_pdf_styles() styles = create_pdf_styles()
elements = create_section_header("Test Header", styles["h1"], add_spacer=False) elements = create_section_header("Test Header", styles["h1"], add_spacer=False)
@@ -864,8 +834,6 @@ class TestExampleReportGenerator:
"""Example concrete implementation for testing.""" """Example concrete implementation for testing."""
def create_executive_summary(self, data): def create_executive_summary(self, data):
from reportlab.platypus import Paragraph
return [ return [
Paragraph("Executive Summary", self.styles["h1"]), Paragraph("Executive Summary", self.styles["h1"]),
Paragraph( Paragraph(
@@ -875,8 +843,6 @@ class TestExampleReportGenerator:
] ]
def create_charts_section(self, data): def create_charts_section(self, data):
from reportlab.platypus import Image
chart_buffer = create_vertical_bar_chart( chart_buffer = create_vertical_bar_chart(
labels=["Pass", "Fail"], labels=["Pass", "Fail"],
values=[80, 20], values=[80, 20],
@@ -884,8 +850,6 @@ class TestExampleReportGenerator:
return [Image(chart_buffer, width=6 * inch, height=4 * inch)] return [Image(chart_buffer, width=6 * inch, height=4 * inch)]
def create_requirements_index(self, data): def create_requirements_index(self, data):
from reportlab.platypus import Paragraph
elements = [Paragraph("Requirements Index", self.styles["h1"])] elements = [Paragraph("Requirements Index", self.styles["h1"])]
for req in data.requirements: for req in data.requirements:
elements.append( elements.append(
@@ -1063,8 +1027,6 @@ class TestComponentEdgeCases:
def test_create_info_table_empty(self): def test_create_info_table_empty(self):
"""Test info table with empty rows.""" """Test info table with empty rows."""
from reportlab.platypus import Table
table = create_info_table([]) table = create_info_table([])
assert isinstance(table, Table) assert isinstance(table, Table)
@@ -1092,8 +1054,6 @@ class TestComponentEdgeCases:
columns = [ColumnConfig("Name", 2 * inch, "name")] columns = [ColumnConfig("Name", 2 * inch, "name")]
table = create_data_table(data, columns) table = create_data_table(data, columns)
# Should be a LongTable for large datasets # Should be a LongTable for large datasets
from reportlab.platypus import LongTable
assert isinstance(table, LongTable) assert isinstance(table, LongTable)
def test_create_risk_component_zero_values(self): def test_create_risk_component_zero_values(self):
@@ -1116,8 +1076,6 @@ class TestColorEdgeCases:
def test_get_color_for_compliance_boundary_60(self): def test_get_color_for_compliance_boundary_60(self):
"""Test compliance color at exactly 60%.""" """Test compliance color at exactly 60%."""
from tasks.jobs.reports import COLOR_LOW_RISK
assert get_color_for_compliance(60) == COLOR_LOW_RISK assert get_color_for_compliance(60) == COLOR_LOW_RISK
def test_get_color_for_compliance_over_100(self): def test_get_color_for_compliance_over_100(self):
@@ -1126,8 +1084,6 @@ class TestColorEdgeCases:
def test_get_color_for_weight_boundary_100(self): def test_get_color_for_weight_boundary_100(self):
"""Test weight color at exactly 100.""" """Test weight color at exactly 100."""
from tasks.jobs.reports import COLOR_LOW_RISK
assert get_color_for_weight(100) == COLOR_LOW_RISK assert get_color_for_weight(100) == COLOR_LOW_RISK
def test_get_color_for_weight_boundary_50(self): def test_get_color_for_weight_boundary_50(self):

View File

@@ -10,16 +10,7 @@ from tasks.jobs.reports import (
ThreatScoreReportGenerator, ThreatScoreReportGenerator,
) )
from api.models import StatusChoices
# Use string status values directly to avoid Django DB initialization
# These match api.models.StatusChoices values
class StatusChoices:
"""Mock StatusChoices to avoid Django DB initialization."""
PASS = "PASS"
FAIL = "FAIL"
MANUAL = "MANUAL"
# ============================================================================= # =============================================================================
# Fixtures # Fixtures