Coverage for lintro / parsers / bandit / bandit_parser.py: 78%
50 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Bandit output parser for security issues."""
3from typing import Any
5from loguru import logger
7from lintro.parsers.bandit.bandit_issue import BanditIssue
8from lintro.parsers.base_parser import validate_int_field, validate_str_field
11def parse_bandit_output(bandit_data: dict[str, Any]) -> list[BanditIssue]:
12 """Parse Bandit JSON output into BanditIssue objects.
14 Args:
15 bandit_data: dict[str, Any]: JSON data from Bandit output.
17 Returns:
18 list[BanditIssue]: List of parsed security issues. Returns empty list
19 if the data structure is invalid.
20 """
21 if not isinstance(bandit_data, dict):
22 logger.warning(
23 "Bandit data must be a dictionary, got {}",
24 type(bandit_data).__name__,
25 )
26 return []
28 results = bandit_data.get("results", [])
29 if not isinstance(results, list):
30 logger.warning(
31 "Bandit results must be a list, got {}",
32 type(results).__name__,
33 )
34 return []
36 issues: list[BanditIssue] = []
38 for result in results:
39 if not isinstance(result, dict):
40 continue
42 try:
43 filename = validate_str_field(
44 result.get("filename"),
45 "filename",
46 log_warning=True,
47 )
48 line_number_raw = result.get("line_number")
49 # Skip if line_number is missing or invalid (required field)
50 # bool is a subclass of int, so we check for bool first
51 if isinstance(line_number_raw, bool):
52 logger.warning("Skipping issue with non-integer line_number")
53 continue
54 if not isinstance(line_number_raw, int):
55 logger.warning("Skipping issue with non-integer line_number")
56 continue
57 # After the isinstance checks above, mypy knows line_number_raw is int
58 line_number: int = line_number_raw
60 # Skip if filename is empty (required field)
61 if not filename:
62 logger.warning("Skipping issue with empty filename")
63 continue
65 col_offset = validate_int_field(result.get("col_offset"), "col_offset")
66 issue_severity = result.get("issue_severity", "UNKNOWN")
67 issue_confidence = result.get("issue_confidence", "UNKNOWN")
68 cwe = result.get("issue_cwe")
69 code = result.get("code")
70 line_range = result.get("line_range")
72 sev = (
73 str(issue_severity).upper() if issue_severity is not None else "UNKNOWN"
74 )
75 conf = (
76 str(issue_confidence).upper()
77 if issue_confidence is not None
78 else "UNKNOWN"
79 )
81 test_id = validate_str_field(result.get("test_id"), "test_id")
82 test_name = validate_str_field(result.get("test_name"), "test_name")
83 issue_text = validate_str_field(result.get("issue_text"), "issue_text")
84 more_info = validate_str_field(result.get("more_info"), "more_info")
86 # Normalize line_range to list[int] when provided
87 if isinstance(line_range, list):
88 line_range = [x for x in line_range if isinstance(x, int)] or None
89 else:
90 line_range = None
92 issue = BanditIssue(
93 file=filename,
94 line=line_number,
95 col_offset=col_offset,
96 issue_severity=sev,
97 issue_confidence=conf,
98 test_id=test_id,
99 test_name=test_name,
100 issue_text=issue_text,
101 more_info=more_info,
102 cwe=cwe if isinstance(cwe, dict) else None,
103 code_snippet=code if isinstance(code, str) else None,
104 line_range=line_range,
105 )
106 issues.append(issue)
107 except (KeyError, TypeError, ValueError) as e:
108 # Log warning but continue processing other issues
109 logger.warning("Failed to parse bandit issue: {}", e)
110 continue
112 return issues