Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support of suppressions #122

Merged
merged 1 commit into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions precli/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,6 @@ def run_checks(parsers: dict, file_list: list[str]) -> list[Result]:

results = []
lines = 0
lines_skipped = 0
for fname in files:
LOG.debug("working on file : %s", fname)

Expand All @@ -174,7 +173,6 @@ def run_checks(parsers: dict, file_list: list[str]) -> list[Result]:
files=len(new_file_list),
files_skipped=len(files_skipped),
lines=lines,
lines_skipped=lines_skipped,
errors=sum(result.level == Level.ERROR for result in results),
warnings=sum(result.level == Level.WARNING for result in results),
notes=sum(result.level == Level.NOTE for result in results),
Expand Down
12 changes: 0 additions & 12 deletions precli/core/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@ def __init__(
files: int,
files_skipped: int,
lines: int,
lines_skipped: int,
errors: int = 0,
warnings: int = 0,
notes: int = 0,
):
self._files = files
self._files_skipped = files_skipped
self._lines = lines
self._lines_skipped = lines_skipped
self._errors = errors
self._warnings = warnings
self._notes = notes
Expand Down Expand Up @@ -50,16 +48,6 @@ def lines(self) -> int:
"""
return self._lines

@property
def lines_skipped(self) -> int:
"""
Total number of lines skipped due to a suppression.

:return: number of lines skipped
:rtype: int
"""
return self._lines_skipped

@property
def errors(self) -> int:
"""
Expand Down
30 changes: 22 additions & 8 deletions precli/core/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def __init__(
location: Location = None,
message: str = None,
fixes: list[Fix] = None,
suppressions: list[Suppression] = None,
suppression: Suppression = None,
):
self._rule_id = rule_id
self._kind = kind
Expand All @@ -32,7 +32,7 @@ def __init__(
else:
self._message = Rule.get_by_id(self._rule_id).message
self._fixes = fixes if fixes is not None else []
self._suppressions = suppressions if suppressions is not None else []
self._suppression = suppression

@property
def rule_id(self) -> str:
Expand Down Expand Up @@ -77,10 +77,12 @@ def level(self) -> Level:
"""
The result severity level.

If the result is being supporessed, then the level is set to NOTE.

:return: severity level
:rtype: Level
"""
return self._level
return self._level if self._suppression is None else Level.NOTE

@property
def message(self) -> str:
Expand All @@ -90,7 +92,10 @@ def message(self) -> str:
:return: issue message
:rtype: str
"""
return self._message
if self._suppression is None:
return self._message
else:
return "This issue is being supporessed via an inline comment."

@property
def rank(self) -> float:
Expand All @@ -116,11 +121,20 @@ def fixes(self) -> list[Fix]:
return self._fixes

@property
def suppressions(self) -> list[Suppression]:
def suppression(self) -> Suppression:
"""
Possible suppressions of the result.

:return: list of suppressions
:rtype: list
:return: suppression or None
:rtype: Suppression
"""
return self._suppression

@suppression.setter
def suppression(self, suppression):
"""
Set the suppression of this result

:param Suppression suppression: suppression
"""
return self._suppressions
self._suppression = suppression
41 changes: 27 additions & 14 deletions precli/core/suppression.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,38 @@
class Suppression:
def __init__(
self,
kind: str,
status: Status = None,
location: Location = None,
location: Location,
rules: set[str],
kind: str = "inSource",
status: Status = Status.ACCEPTED,
justification: str = None,
):
self._location = location
self._rules = rules
self._kind = kind
self._status = status
self._location = location
self._justification = justification

@property
def location(self) -> Location:
"""
Specifies the location of the suppression.

:return: location of suppression
:rtype: Location
"""
return self._location

@property
def rules(self) -> set[str]:
"""
What rules are being suppressed.

:return: set of rule ID/names
:rtype: set
"""
return self._rules

@property
def kind(self) -> str:
"""
Expand All @@ -26,7 +48,7 @@ def kind(self) -> str:
:return: kind of suppression
:rtype: str
"""
return "inSource"
return self._kind

@property
def status(self) -> Status:
Expand All @@ -39,15 +61,6 @@ def status(self) -> Status:
return self._status

@property
def location(self) -> Location:
"""
Specifies the location of the suppression.

:return: location of suppression
:rtype: Location
"""
return self._location

def justification(self) -> str:
"""
User-supplied string that explains why the result was suppressed.
Expand Down
14 changes: 10 additions & 4 deletions precli/parsers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ def __init__(self, lang: str, enabled: list = None, disabled: list = None):
:param list enabled: list of rules to enable
:param list disabled: list of rules to disable
"""
self.language = tree_sitter_languages.get_language(lang)
self.parser = tree_sitter_languages.get_parser(lang)
self.tree_sitter_language = tree_sitter_languages.get_language(lang)
self.tree_sitter_parser = tree_sitter_languages.get_parser(lang)
self.rules = {}
self.wildcards = {}

Expand Down Expand Up @@ -77,8 +77,14 @@ def parse(self, file_name: str, data: bytes = None) -> list[Result]:
if data is None:
with open(file_name, "rb") as fdata:
data = fdata.read()
tree = self.parser.parse(data)
tree = self.tree_sitter_parser.parse(data)
self.visit([tree.root_node])

for result in self.results:
suppression = self.suppressions.get(result.location.start_line)
if suppression and result.rule_id in suppression.rules:
result.suppression = suppression

return self.results

def visit(self, nodes: list[Node]):
Expand Down Expand Up @@ -112,5 +118,5 @@ def process_rules(self, target: str, **kwargs: dict) -> list[Result]:
context = self.context
context["symtab"] = self.current_symtab
result = rule.analyze(self.context, **kwargs)
if result:
if result is not None:
self.results.append(result)
44 changes: 43 additions & 1 deletion precli/parsers/python.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
# Copyright 2023 Secure Saurce LLC
import ast
import re
from collections import namedtuple

from tree_sitter import Node

from precli.core.call import Call
from precli.core.comparison import Comparison
from precli.core.location import Location
from precli.core.suppression import Suppression
from precli.core.symtab import Symbol
from precli.core.symtab import SymbolTable
from precli.parsers import Parser
from precli.rules import Rule


Import = namedtuple("Import", "module alias")

SUPPRESS_COMMENT = re.compile(r"# suppress:? (?P<rules>[^#]+)?#?")
SUPPRESSED_RULES = re.compile(r"(?:(PRE\d+|[a-z_]+),?)+", re.IGNORECASE)


class Python(Parser):
def __init__(self, enabled: list = None, disabled: list = None):
Expand All @@ -22,6 +29,7 @@ def file_extension(self) -> str:
return ".py"

def visit_module(self, nodes: list[Node]):
self.suppressions = {}
self.current_symtab = SymbolTable("<module>")
self.visit(nodes)
self.current_symtab = self.current_symtab.parent()
Expand All @@ -37,7 +45,41 @@ def visit_import_from_statement(self, nodes: list[Node]):
self.current_symtab.put(key, "import", value)

def visit_comment(self, nodes: list[Node]):
pass
comment = self.context["node"].text.decode()

suppressed = SUPPRESS_COMMENT.search(comment)
if suppressed is None:
return

matches = suppressed.groupdict()
suppressed_rules = matches.get("rules")

if suppressed_rules is None:
return

rules = set()
for rule in SUPPRESSED_RULES.finditer(suppressed_rules):
rule_name_or_id = rule.group(1)
if Rule.get_by_id(rule_name_or_id) is not None:
rules.add(rule_name_or_id)

if not rules:
return

suppression = Suppression(
location=Location(node=self.context["node"]),
rules=rules,
)

prev_node = self.context["node"].prev_sibling
node = self.context["node"]

if prev_node.end_point[0] == node.start_point[0]:
self.suppressions[node.start_point[0] + 1] = suppression
else:
self.suppressions[node.start_point[0] + 2] = suppression

# TODO: add the justification to the suppression

def visit_class_definition(self, nodes: list[Node]):
class_id = self.first_match(self.context["node"], "identifier")
Expand Down
Loading
Loading