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

1"""Bandit output parser for security issues.""" 

2 

3from typing import Any 

4 

5from loguru import logger 

6 

7from lintro.parsers.bandit.bandit_issue import BanditIssue 

8from lintro.parsers.base_parser import validate_int_field, validate_str_field 

9 

10 

11def parse_bandit_output(bandit_data: dict[str, Any]) -> list[BanditIssue]: 

12 """Parse Bandit JSON output into BanditIssue objects. 

13 

14 Args: 

15 bandit_data: dict[str, Any]: JSON data from Bandit output. 

16 

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 [] 

27 

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 [] 

35 

36 issues: list[BanditIssue] = [] 

37 

38 for result in results: 

39 if not isinstance(result, dict): 

40 continue 

41 

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 

59 

60 # Skip if filename is empty (required field) 

61 if not filename: 

62 logger.warning("Skipping issue with empty filename") 

63 continue 

64 

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") 

71 

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 ) 

80 

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") 

85 

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 

91 

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 

111 

112 return issues