slither.core.slither_core

Main module

  1"""
  2    Main module
  3"""
  4import json
  5import logging
  6import os
  7import pathlib
  8import posixpath
  9import re
 10from collections import defaultdict
 11from typing import Optional, Dict, List, Set, Union, Tuple
 12
 13from crytic_compile import CryticCompile
 14from crytic_compile.utils.naming import Filename
 15
 16from slither.core.declarations.contract_level import ContractLevel
 17from slither.core.compilation_unit import SlitherCompilationUnit
 18from slither.core.context.context import Context
 19from slither.core.declarations import Contract, FunctionContract
 20from slither.core.declarations.top_level import TopLevel
 21from slither.core.source_mapping.source_mapping import SourceMapping, Source
 22from slither.slithir.variables import Constant
 23from slither.utils.colors import red
 24from slither.utils.sarif import read_triage_info
 25from slither.utils.source_mapping import get_definition, get_references, get_all_implementations
 26
 27logger = logging.getLogger("Slither")
 28logging.basicConfig()
 29
 30
 31def _relative_path_format(path: str) -> str:
 32    """
 33    Strip relative paths of "." and ".."
 34    """
 35    return path.split("..")[-1].strip(".").strip("/")
 36
 37
 38# pylint: disable=too-many-instance-attributes,too-many-public-methods
 39class SlitherCore(Context):
 40    """
 41    Slither static analyzer
 42    """
 43
 44    def __init__(self) -> None:
 45        super().__init__()
 46
 47        self._filename: Optional[str] = None
 48        self._raw_source_code: Dict[str, str] = {}
 49        self._source_code_to_line: Optional[Dict[str, List[str]]] = None
 50
 51        self._previous_results_filename: str = "slither.db.json"
 52
 53        # TODO: add cli flag to set these variables
 54        self.sarif_input: str = "export.sarif"
 55        self.sarif_triage: str = "export.sarif.sarifexplorer"
 56        self._results_to_hide: List = []
 57        self._previous_results: List = []
 58        # From triaged result
 59        self._previous_results_ids: Set[str] = set()
 60        # Every slither object has a list of result from detector
 61        # Because of the multiple compilation support, we might analyze
 62        # Multiple time the same result, so we remove duplicates
 63        self._currently_seen_resuts: Set[str] = set()
 64        self._paths_to_filter: Set[str] = set()
 65        self._paths_to_include: Set[str] = set()
 66
 67        self._crytic_compile: Optional[CryticCompile] = None
 68
 69        self._generate_patches = False
 70        self._exclude_dependencies = False
 71
 72        self._markdown_root = ""
 73
 74        # If set to true, slither will not catch errors during parsing
 75        self._disallow_partial: bool = False
 76        self._skip_assembly: bool = False
 77
 78        self._show_ignored_findings = False
 79
 80        # Maps from file to detector name to the start/end ranges for that detector.
 81        # Infinity is used to signal a detector has no end range.
 82        self._ignore_ranges: Dict[str, Dict[str, List[Tuple[int, ...]]]] = defaultdict(
 83            lambda: defaultdict(lambda: [(-1, -1)])
 84        )
 85
 86        self._compilation_units: List[SlitherCompilationUnit] = []
 87
 88        self._contracts: List[Contract] = []
 89        self._contracts_derived: List[Contract] = []
 90
 91        self._offset_to_objects: Optional[Dict[Filename, Dict[int, Set[SourceMapping]]]] = None
 92        self._offset_to_references: Optional[Dict[Filename, Dict[int, Set[Source]]]] = None
 93        self._offset_to_implementations: Optional[Dict[Filename, Dict[int, Set[Source]]]] = None
 94        self._offset_to_definitions: Optional[Dict[Filename, Dict[int, Set[Source]]]] = None
 95
 96        # Line prefix is used during the source mapping generation
 97        # By default we generate file.sol#1
 98        # But we allow to alter this (ex: file.sol:1) for vscode integration
 99        self.line_prefix: str = "#"
100
101        # Use by the echidna printer
102        # If true, partial analysis is allowed
103        self.no_fail = False
104
105        self.skip_data_dependency = False
106
107    @property
108    def compilation_units(self) -> List[SlitherCompilationUnit]:
109        return list(self._compilation_units)
110
111    def add_compilation_unit(self, compilation_unit: SlitherCompilationUnit):
112        self._compilation_units.append(compilation_unit)
113
114    # endregion
115    ###################################################################################
116    ###################################################################################
117    # region Contracts
118    ###################################################################################
119    ###################################################################################
120
121    @property
122    def contracts(self) -> List[Contract]:
123        if not self._contracts:
124            all_contracts = [
125                compilation_unit.contracts for compilation_unit in self._compilation_units
126            ]
127            self._contracts = [item for sublist in all_contracts for item in sublist]
128        return self._contracts
129
130    @property
131    def contracts_derived(self) -> List[Contract]:
132        if not self._contracts_derived:
133            all_contracts = [
134                compilation_unit.contracts_derived for compilation_unit in self._compilation_units
135            ]
136            self._contracts_derived = [item for sublist in all_contracts for item in sublist]
137        return self._contracts_derived
138
139    def get_contract_from_name(self, contract_name: Union[str, Constant]) -> List[Contract]:
140        """
141            Return a contract from a name
142        Args:
143            contract_name (str): name of the contract
144        Returns:
145            Contract
146        """
147        contracts = []
148        for compilation_unit in self._compilation_units:
149            contracts += compilation_unit.get_contract_from_name(contract_name)
150        return contracts
151
152    ###################################################################################
153    ###################################################################################
154    # region Source code
155    ###################################################################################
156    ###################################################################################
157
158    @property
159    def source_code(self) -> Dict[str, str]:
160        """{filename: source_code (str)}: source code"""
161        return self._raw_source_code
162
163    @property
164    def filename(self) -> Optional[str]:
165        """str: Filename."""
166        return self._filename
167
168    @filename.setter
169    def filename(self, filename: str):
170        self._filename = filename
171
172    def add_source_code(self, path: str) -> None:
173        """
174        :param path:
175        :return:
176        """
177        if self.crytic_compile and path in self.crytic_compile.src_content:
178            self.source_code[path] = self.crytic_compile.src_content[path]
179        else:
180            with open(path, encoding="utf8", newline="") as f:
181                self.source_code[path] = f.read()
182
183        self.parse_ignore_comments(path)
184
185    @property
186    def markdown_root(self) -> str:
187        return self._markdown_root
188
189    def print_functions(self, d: str):
190        """
191        Export all the functions to dot files
192        """
193        for compilation_unit in self._compilation_units:
194            for c in compilation_unit.contracts:
195                for f in c.functions:
196                    f.cfg_to_dot(os.path.join(d, f"{c.name}.{f.name}.dot"))
197
198    def offset_to_objects(self, filename_str: str, offset: int) -> Set[SourceMapping]:
199        if self._offset_to_objects is None:
200            self._compute_offsets_to_ref_impl_decl()
201        filename: Filename = self.crytic_compile.filename_lookup(filename_str)
202        return self._offset_to_objects[filename][offset]
203
204    def _compute_offsets_from_thing(self, thing: SourceMapping):
205        definition = get_definition(thing, self.crytic_compile)
206        references = get_references(thing)
207        implementations = get_all_implementations(thing, self.contracts)
208
209        for offset in range(definition.start, definition.end + 1):
210            if (
211                isinstance(thing, (TopLevel, Contract))
212                or (
213                    isinstance(thing, FunctionContract)
214                    and thing.contract_declarer == thing.contract
215                )
216                or (isinstance(thing, ContractLevel) and not isinstance(thing, FunctionContract))
217            ):
218
219                self._offset_to_objects[definition.filename][offset].add(thing)
220
221            self._offset_to_definitions[definition.filename][offset].add(definition)
222            self._offset_to_implementations[definition.filename][offset].update(implementations)
223            self._offset_to_references[definition.filename][offset] |= set(references)
224
225        for ref in references:
226            for offset in range(ref.start, ref.end + 1):
227                is_declared_function = (
228                    isinstance(thing, FunctionContract)
229                    and thing.contract_declarer == thing.contract
230                )
231                if (
232                    isinstance(thing, TopLevel)
233                    or is_declared_function
234                    or (
235                        isinstance(thing, ContractLevel) and not isinstance(thing, FunctionContract)
236                    )
237                ):
238                    self._offset_to_objects[definition.filename][offset].add(thing)
239
240                if is_declared_function:
241                    # Only show the nearest lexical definition for declared contract-level functions
242                    if (
243                        thing.contract.source_mapping.start
244                        < offset
245                        < thing.contract.source_mapping.end
246                    ):
247
248                        self._offset_to_definitions[ref.filename][offset].add(definition)
249
250                else:
251                    self._offset_to_definitions[ref.filename][offset].add(definition)
252
253                self._offset_to_implementations[ref.filename][offset].update(implementations)
254                self._offset_to_references[ref.filename][offset] |= set(references)
255
256    def _compute_offsets_to_ref_impl_decl(self):  # pylint: disable=too-many-branches
257        self._offset_to_references = defaultdict(lambda: defaultdict(lambda: set()))
258        self._offset_to_definitions = defaultdict(lambda: defaultdict(lambda: set()))
259        self._offset_to_implementations = defaultdict(lambda: defaultdict(lambda: set()))
260        self._offset_to_objects = defaultdict(lambda: defaultdict(lambda: set()))
261
262        for compilation_unit in self._compilation_units:
263            for contract in compilation_unit.contracts:
264                self._compute_offsets_from_thing(contract)
265
266                for function in contract.functions_declared:
267                    self._compute_offsets_from_thing(function)
268                    for variable in function.local_variables:
269                        self._compute_offsets_from_thing(variable)
270                for modifier in contract.modifiers_declared:
271                    self._compute_offsets_from_thing(modifier)
272                    for variable in modifier.local_variables:
273                        self._compute_offsets_from_thing(variable)
274
275                for var in contract.state_variables:
276                    self._compute_offsets_from_thing(var)
277
278                for st in contract.structures:
279                    self._compute_offsets_from_thing(st)
280
281                for enum in contract.enums:
282                    self._compute_offsets_from_thing(enum)
283
284                for event in contract.events:
285                    self._compute_offsets_from_thing(event)
286
287                for typ in contract.type_aliases:
288                    self._compute_offsets_from_thing(typ)
289
290            for enum in compilation_unit.enums_top_level:
291                self._compute_offsets_from_thing(enum)
292            for event in compilation_unit.events_top_level:
293                self._compute_offsets_from_thing(event)
294            for function in compilation_unit.functions_top_level:
295                self._compute_offsets_from_thing(function)
296            for st in compilation_unit.structures_top_level:
297                self._compute_offsets_from_thing(st)
298            for var in compilation_unit.variables_top_level:
299                self._compute_offsets_from_thing(var)
300            for typ in compilation_unit.type_aliases.values():
301                self._compute_offsets_from_thing(typ)
302            for err in compilation_unit.custom_errors:
303                self._compute_offsets_from_thing(err)
304            for event in compilation_unit.events_top_level:
305                self._compute_offsets_from_thing(event)
306            for import_directive in compilation_unit.import_directives:
307                self._compute_offsets_from_thing(import_directive)
308            for pragma in compilation_unit.pragma_directives:
309                self._compute_offsets_from_thing(pragma)
310
311    def offset_to_references(self, filename_str: str, offset: int) -> Set[Source]:
312        if self._offset_to_references is None:
313            self._compute_offsets_to_ref_impl_decl()
314        filename: Filename = self.crytic_compile.filename_lookup(filename_str)
315        return self._offset_to_references[filename][offset]
316
317    def offset_to_implementations(self, filename_str: str, offset: int) -> Set[Source]:
318        if self._offset_to_implementations is None:
319            self._compute_offsets_to_ref_impl_decl()
320        filename: Filename = self.crytic_compile.filename_lookup(filename_str)
321        return self._offset_to_implementations[filename][offset]
322
323    def offset_to_definitions(self, filename_str: str, offset: int) -> Set[Source]:
324        if self._offset_to_definitions is None:
325            self._compute_offsets_to_ref_impl_decl()
326        filename: Filename = self.crytic_compile.filename_lookup(filename_str)
327        return self._offset_to_definitions[filename][offset]
328
329    # endregion
330    ###################################################################################
331    ###################################################################################
332    # region Filtering results
333    ###################################################################################
334    ###################################################################################
335
336    def parse_ignore_comments(self, file: str) -> None:
337        # The first time we check a file, find all start/end ignore comments and memoize them.
338        line_number = 1
339        while True:
340
341            line_text = self.crytic_compile.get_code_from_line(file, line_number)
342            if line_text is None:
343                break
344
345            start_regex = r"^\s*//\s*slither-disable-start\s*([a-zA-Z0-9_,-]*)"
346            end_regex = r"^\s*//\s*slither-disable-end\s*([a-zA-Z0-9_,-]*)"
347            start_match = re.findall(start_regex, line_text.decode("utf8"))
348            end_match = re.findall(end_regex, line_text.decode("utf8"))
349
350            if start_match:
351                ignored = start_match[0].split(",")
352                if ignored:
353                    for check in ignored:
354                        vals = self._ignore_ranges[file][check]
355                        if len(vals) == 0 or vals[-1][1] != float("inf"):
356                            # First item in the array, or the prior item is fully populated.
357                            self._ignore_ranges[file][check].append((line_number, float("inf")))
358                        else:
359                            logger.error(
360                                f"Consecutive slither-disable-starts without slither-disable-end in {file}#{line_number}"
361                            )
362                            return
363
364            if end_match:
365                ignored = end_match[0].split(",")
366                if ignored:
367                    for check in ignored:
368                        vals = self._ignore_ranges[file][check]
369                        if len(vals) == 0 or vals[-1][1] != float("inf"):
370                            logger.error(
371                                f"slither-disable-end without slither-disable-start in {file}#{line_number}"
372                            )
373                            return
374                        self._ignore_ranges[file][check][-1] = (vals[-1][0], line_number)
375
376            line_number += 1
377
378    def has_ignore_comment(self, r: Dict) -> bool:
379        """
380        Check if the result has an ignore comment in the file or on the preceding line, in which
381        case, it is not valid
382        """
383        if not self.crytic_compile:
384            return False
385        mapping_elements_with_lines = (
386            (
387                posixpath.normpath(elem["source_mapping"]["filename_absolute"]),
388                elem["source_mapping"]["lines"],
389            )
390            for elem in r["elements"]
391            if "source_mapping" in elem
392            and "filename_absolute" in elem["source_mapping"]
393            and "lines" in elem["source_mapping"]
394            and len(elem["source_mapping"]["lines"]) > 0
395        )
396
397        for file, lines in mapping_elements_with_lines:
398
399            # Check if result is within an ignored range.
400            ignore_ranges = self._ignore_ranges[file][r["check"]] + self._ignore_ranges[file]["all"]
401            for start, end in ignore_ranges:
402                # The full check must be within the ignore range to be ignored.
403                if start < lines[0] and end > lines[-1]:
404                    return True
405
406            # Check for next-line matchers.
407            ignore_line_index = min(lines) - 1
408            ignore_line_text = self.crytic_compile.get_code_from_line(file, ignore_line_index)
409            if ignore_line_text:
410                match = re.findall(
411                    r"^\s*//\s*slither-disable-next-line\s*([a-zA-Z0-9_,-]*)",
412                    ignore_line_text.decode("utf8"),
413                )
414                if match:
415                    ignored = match[0].split(",")
416                    if ignored and ("all" in ignored or any(r["check"] == c for c in ignored)):
417                        return True
418
419        return False
420
421    def valid_result(self, r: Dict) -> bool:
422        """
423        Check if the result is valid
424        A result is invalid if:
425            - All its source paths belong to the source path filtered
426            - Or a similar result was reported and saved during a previous run
427            - The --exclude-dependencies flag is set and results are only related to dependencies
428            - There is an ignore comment on the preceding line or in the file
429        """
430
431        # Remove duplicate due to the multiple compilation support
432        if r["id"] in self._currently_seen_resuts:
433            return False
434        self._currently_seen_resuts.add(r["id"])
435
436        source_mapping_elements = [
437            elem["source_mapping"].get("filename_absolute", "unknown")
438            for elem in r["elements"]
439            if "source_mapping" in elem
440        ]
441
442        # Use POSIX-style paths so that filter_paths|include_paths works across different
443        # OSes. Convert to a list so elements don't get consumed and are lost
444        # while evaluating the first pattern
445        source_mapping_elements = list(
446            map(lambda x: pathlib.Path(x).resolve().as_posix() if x else x, source_mapping_elements)
447        )
448        (matching, paths, msg_err) = (
449            (True, self._paths_to_include, "--include-paths")
450            if self._paths_to_include
451            else (False, self._paths_to_filter, "--filter-paths")
452        )
453
454        for path in paths:
455            try:
456                if any(
457                    bool(re.search(_relative_path_format(path), src_mapping))
458                    for src_mapping in source_mapping_elements
459                ):
460                    matching = not matching
461                    break
462            except re.error:
463                logger.error(
464                    f"Incorrect regular expression for {msg_err} {path}."
465                    "\nSlither supports the Python re format"
466                    ": https://docs.python.org/3/library/re.html"
467                )
468
469        if r["elements"] and matching:
470            return False
471
472        if self._show_ignored_findings:
473            return True
474        if self.has_ignore_comment(r):
475            return False
476        if r["id"] in self._previous_results_ids:
477            return False
478        if r["elements"] and self._exclude_dependencies:
479            if all(element["source_mapping"]["is_dependency"] for element in r["elements"]):
480                return False
481        # Conserve previous result filtering. This is conserved for compatibility, but is meant to be removed
482        if r["description"] in [pr["description"] for pr in self._previous_results]:
483            return False
484
485        return True
486
487    def load_previous_results(self) -> None:
488        self.load_previous_results_from_sarif()
489
490        filename = self._previous_results_filename
491        try:
492            if os.path.isfile(filename):
493                with open(filename, encoding="utf8") as f:
494                    self._previous_results = json.load(f)
495                    if self._previous_results:
496                        for r in self._previous_results:
497                            if "id" in r:
498                                self._previous_results_ids.add(r["id"])
499
500        except json.decoder.JSONDecodeError:
501            logger.error(red(f"Impossible to decode {filename}. Consider removing the file"))
502
503    def load_previous_results_from_sarif(self) -> None:
504        sarif = pathlib.Path(self.sarif_input)
505        triage = pathlib.Path(self.sarif_triage)
506
507        if not sarif.exists():
508            return
509        if not triage.exists():
510            return
511
512        triaged = read_triage_info(sarif, triage)
513
514        for id_triaged in triaged:
515            self._previous_results_ids.add(id_triaged)
516
517    def write_results_to_hide(self) -> None:
518        if not self._results_to_hide:
519            return
520        filename = self._previous_results_filename
521        with open(filename, "w", encoding="utf8") as f:
522            results = self._results_to_hide + self._previous_results
523            json.dump(results, f)
524
525    def save_results_to_hide(self, results: List[Dict]) -> None:
526        self._results_to_hide += results
527
528    def add_path_to_filter(self, path: str):
529        """
530        Add path to filter
531        Path are used through direct comparison (no regex)
532        """
533        self._paths_to_filter.add(path)
534
535    def add_path_to_include(self, path: str):
536        """
537        Add path to include
538        Path are used through direct comparison (no regex)
539        """
540        self._paths_to_include.add(path)
541
542    # endregion
543    ###################################################################################
544    ###################################################################################
545    # region Crytic compile
546    ###################################################################################
547    ###################################################################################
548
549    @property
550    def crytic_compile(self) -> CryticCompile:
551        return self._crytic_compile  # type: ignore
552
553    # endregion
554    ###################################################################################
555    ###################################################################################
556    # region Format
557    ###################################################################################
558    ###################################################################################
559
560    @property
561    def generate_patches(self) -> bool:
562        return self._generate_patches
563
564    @generate_patches.setter
565    def generate_patches(self, p: bool):
566        self._generate_patches = p
567
568    # endregion
569    ###################################################################################
570    ###################################################################################
571    # region Internals
572    ###################################################################################
573    ###################################################################################
574
575    @property
576    def disallow_partial(self) -> bool:
577        """
578        Return true if partial analyses are disallowed
579        For example, codebase with duplicate names will lead to partial analyses
580
581        :return:
582        """
583        return self._disallow_partial
584
585    @property
586    def skip_assembly(self) -> bool:
587        return self._skip_assembly
588
589    @property
590    def show_ignore_findings(self) -> bool:
591        return self._show_ignored_findings
592
593    # endregion
logger = <Logger Slither (WARNING)>
class SlitherCore(slither.core.context.context.Context):
 40class SlitherCore(Context):
 41    """
 42    Slither static analyzer
 43    """
 44
 45    def __init__(self) -> None:
 46        super().__init__()
 47
 48        self._filename: Optional[str] = None
 49        self._raw_source_code: Dict[str, str] = {}
 50        self._source_code_to_line: Optional[Dict[str, List[str]]] = None
 51
 52        self._previous_results_filename: str = "slither.db.json"
 53
 54        # TODO: add cli flag to set these variables
 55        self.sarif_input: str = "export.sarif"
 56        self.sarif_triage: str = "export.sarif.sarifexplorer"
 57        self._results_to_hide: List = []
 58        self._previous_results: List = []
 59        # From triaged result
 60        self._previous_results_ids: Set[str] = set()
 61        # Every slither object has a list of result from detector
 62        # Because of the multiple compilation support, we might analyze
 63        # Multiple time the same result, so we remove duplicates
 64        self._currently_seen_resuts: Set[str] = set()
 65        self._paths_to_filter: Set[str] = set()
 66        self._paths_to_include: Set[str] = set()
 67
 68        self._crytic_compile: Optional[CryticCompile] = None
 69
 70        self._generate_patches = False
 71        self._exclude_dependencies = False
 72
 73        self._markdown_root = ""
 74
 75        # If set to true, slither will not catch errors during parsing
 76        self._disallow_partial: bool = False
 77        self._skip_assembly: bool = False
 78
 79        self._show_ignored_findings = False
 80
 81        # Maps from file to detector name to the start/end ranges for that detector.
 82        # Infinity is used to signal a detector has no end range.
 83        self._ignore_ranges: Dict[str, Dict[str, List[Tuple[int, ...]]]] = defaultdict(
 84            lambda: defaultdict(lambda: [(-1, -1)])
 85        )
 86
 87        self._compilation_units: List[SlitherCompilationUnit] = []
 88
 89        self._contracts: List[Contract] = []
 90        self._contracts_derived: List[Contract] = []
 91
 92        self._offset_to_objects: Optional[Dict[Filename, Dict[int, Set[SourceMapping]]]] = None
 93        self._offset_to_references: Optional[Dict[Filename, Dict[int, Set[Source]]]] = None
 94        self._offset_to_implementations: Optional[Dict[Filename, Dict[int, Set[Source]]]] = None
 95        self._offset_to_definitions: Optional[Dict[Filename, Dict[int, Set[Source]]]] = None
 96
 97        # Line prefix is used during the source mapping generation
 98        # By default we generate file.sol#1
 99        # But we allow to alter this (ex: file.sol:1) for vscode integration
100        self.line_prefix: str = "#"
101
102        # Use by the echidna printer
103        # If true, partial analysis is allowed
104        self.no_fail = False
105
106        self.skip_data_dependency = False
107
108    @property
109    def compilation_units(self) -> List[SlitherCompilationUnit]:
110        return list(self._compilation_units)
111
112    def add_compilation_unit(self, compilation_unit: SlitherCompilationUnit):
113        self._compilation_units.append(compilation_unit)
114
115    # endregion
116    ###################################################################################
117    ###################################################################################
118    # region Contracts
119    ###################################################################################
120    ###################################################################################
121
122    @property
123    def contracts(self) -> List[Contract]:
124        if not self._contracts:
125            all_contracts = [
126                compilation_unit.contracts for compilation_unit in self._compilation_units
127            ]
128            self._contracts = [item for sublist in all_contracts for item in sublist]
129        return self._contracts
130
131    @property
132    def contracts_derived(self) -> List[Contract]:
133        if not self._contracts_derived:
134            all_contracts = [
135                compilation_unit.contracts_derived for compilation_unit in self._compilation_units
136            ]
137            self._contracts_derived = [item for sublist in all_contracts for item in sublist]
138        return self._contracts_derived
139
140    def get_contract_from_name(self, contract_name: Union[str, Constant]) -> List[Contract]:
141        """
142            Return a contract from a name
143        Args:
144            contract_name (str): name of the contract
145        Returns:
146            Contract
147        """
148        contracts = []
149        for compilation_unit in self._compilation_units:
150            contracts += compilation_unit.get_contract_from_name(contract_name)
151        return contracts
152
153    ###################################################################################
154    ###################################################################################
155    # region Source code
156    ###################################################################################
157    ###################################################################################
158
159    @property
160    def source_code(self) -> Dict[str, str]:
161        """{filename: source_code (str)}: source code"""
162        return self._raw_source_code
163
164    @property
165    def filename(self) -> Optional[str]:
166        """str: Filename."""
167        return self._filename
168
169    @filename.setter
170    def filename(self, filename: str):
171        self._filename = filename
172
173    def add_source_code(self, path: str) -> None:
174        """
175        :param path:
176        :return:
177        """
178        if self.crytic_compile and path in self.crytic_compile.src_content:
179            self.source_code[path] = self.crytic_compile.src_content[path]
180        else:
181            with open(path, encoding="utf8", newline="") as f:
182                self.source_code[path] = f.read()
183
184        self.parse_ignore_comments(path)
185
186    @property
187    def markdown_root(self) -> str:
188        return self._markdown_root
189
190    def print_functions(self, d: str):
191        """
192        Export all the functions to dot files
193        """
194        for compilation_unit in self._compilation_units:
195            for c in compilation_unit.contracts:
196                for f in c.functions:
197                    f.cfg_to_dot(os.path.join(d, f"{c.name}.{f.name}.dot"))
198
199    def offset_to_objects(self, filename_str: str, offset: int) -> Set[SourceMapping]:
200        if self._offset_to_objects is None:
201            self._compute_offsets_to_ref_impl_decl()
202        filename: Filename = self.crytic_compile.filename_lookup(filename_str)
203        return self._offset_to_objects[filename][offset]
204
205    def _compute_offsets_from_thing(self, thing: SourceMapping):
206        definition = get_definition(thing, self.crytic_compile)
207        references = get_references(thing)
208        implementations = get_all_implementations(thing, self.contracts)
209
210        for offset in range(definition.start, definition.end + 1):
211            if (
212                isinstance(thing, (TopLevel, Contract))
213                or (
214                    isinstance(thing, FunctionContract)
215                    and thing.contract_declarer == thing.contract
216                )
217                or (isinstance(thing, ContractLevel) and not isinstance(thing, FunctionContract))
218            ):
219
220                self._offset_to_objects[definition.filename][offset].add(thing)
221
222            self._offset_to_definitions[definition.filename][offset].add(definition)
223            self._offset_to_implementations[definition.filename][offset].update(implementations)
224            self._offset_to_references[definition.filename][offset] |= set(references)
225
226        for ref in references:
227            for offset in range(ref.start, ref.end + 1):
228                is_declared_function = (
229                    isinstance(thing, FunctionContract)
230                    and thing.contract_declarer == thing.contract
231                )
232                if (
233                    isinstance(thing, TopLevel)
234                    or is_declared_function
235                    or (
236                        isinstance(thing, ContractLevel) and not isinstance(thing, FunctionContract)
237                    )
238                ):
239                    self._offset_to_objects[definition.filename][offset].add(thing)
240
241                if is_declared_function:
242                    # Only show the nearest lexical definition for declared contract-level functions
243                    if (
244                        thing.contract.source_mapping.start
245                        < offset
246                        < thing.contract.source_mapping.end
247                    ):
248
249                        self._offset_to_definitions[ref.filename][offset].add(definition)
250
251                else:
252                    self._offset_to_definitions[ref.filename][offset].add(definition)
253
254                self._offset_to_implementations[ref.filename][offset].update(implementations)
255                self._offset_to_references[ref.filename][offset] |= set(references)
256
257    def _compute_offsets_to_ref_impl_decl(self):  # pylint: disable=too-many-branches
258        self._offset_to_references = defaultdict(lambda: defaultdict(lambda: set()))
259        self._offset_to_definitions = defaultdict(lambda: defaultdict(lambda: set()))
260        self._offset_to_implementations = defaultdict(lambda: defaultdict(lambda: set()))
261        self._offset_to_objects = defaultdict(lambda: defaultdict(lambda: set()))
262
263        for compilation_unit in self._compilation_units:
264            for contract in compilation_unit.contracts:
265                self._compute_offsets_from_thing(contract)
266
267                for function in contract.functions_declared:
268                    self._compute_offsets_from_thing(function)
269                    for variable in function.local_variables:
270                        self._compute_offsets_from_thing(variable)
271                for modifier in contract.modifiers_declared:
272                    self._compute_offsets_from_thing(modifier)
273                    for variable in modifier.local_variables:
274                        self._compute_offsets_from_thing(variable)
275
276                for var in contract.state_variables:
277                    self._compute_offsets_from_thing(var)
278
279                for st in contract.structures:
280                    self._compute_offsets_from_thing(st)
281
282                for enum in contract.enums:
283                    self._compute_offsets_from_thing(enum)
284
285                for event in contract.events:
286                    self._compute_offsets_from_thing(event)
287
288                for typ in contract.type_aliases:
289                    self._compute_offsets_from_thing(typ)
290
291            for enum in compilation_unit.enums_top_level:
292                self._compute_offsets_from_thing(enum)
293            for event in compilation_unit.events_top_level:
294                self._compute_offsets_from_thing(event)
295            for function in compilation_unit.functions_top_level:
296                self._compute_offsets_from_thing(function)
297            for st in compilation_unit.structures_top_level:
298                self._compute_offsets_from_thing(st)
299            for var in compilation_unit.variables_top_level:
300                self._compute_offsets_from_thing(var)
301            for typ in compilation_unit.type_aliases.values():
302                self._compute_offsets_from_thing(typ)
303            for err in compilation_unit.custom_errors:
304                self._compute_offsets_from_thing(err)
305            for event in compilation_unit.events_top_level:
306                self._compute_offsets_from_thing(event)
307            for import_directive in compilation_unit.import_directives:
308                self._compute_offsets_from_thing(import_directive)
309            for pragma in compilation_unit.pragma_directives:
310                self._compute_offsets_from_thing(pragma)
311
312    def offset_to_references(self, filename_str: str, offset: int) -> Set[Source]:
313        if self._offset_to_references is None:
314            self._compute_offsets_to_ref_impl_decl()
315        filename: Filename = self.crytic_compile.filename_lookup(filename_str)
316        return self._offset_to_references[filename][offset]
317
318    def offset_to_implementations(self, filename_str: str, offset: int) -> Set[Source]:
319        if self._offset_to_implementations is None:
320            self._compute_offsets_to_ref_impl_decl()
321        filename: Filename = self.crytic_compile.filename_lookup(filename_str)
322        return self._offset_to_implementations[filename][offset]
323
324    def offset_to_definitions(self, filename_str: str, offset: int) -> Set[Source]:
325        if self._offset_to_definitions is None:
326            self._compute_offsets_to_ref_impl_decl()
327        filename: Filename = self.crytic_compile.filename_lookup(filename_str)
328        return self._offset_to_definitions[filename][offset]
329
330    # endregion
331    ###################################################################################
332    ###################################################################################
333    # region Filtering results
334    ###################################################################################
335    ###################################################################################
336
337    def parse_ignore_comments(self, file: str) -> None:
338        # The first time we check a file, find all start/end ignore comments and memoize them.
339        line_number = 1
340        while True:
341
342            line_text = self.crytic_compile.get_code_from_line(file, line_number)
343            if line_text is None:
344                break
345
346            start_regex = r"^\s*//\s*slither-disable-start\s*([a-zA-Z0-9_,-]*)"
347            end_regex = r"^\s*//\s*slither-disable-end\s*([a-zA-Z0-9_,-]*)"
348            start_match = re.findall(start_regex, line_text.decode("utf8"))
349            end_match = re.findall(end_regex, line_text.decode("utf8"))
350
351            if start_match:
352                ignored = start_match[0].split(",")
353                if ignored:
354                    for check in ignored:
355                        vals = self._ignore_ranges[file][check]
356                        if len(vals) == 0 or vals[-1][1] != float("inf"):
357                            # First item in the array, or the prior item is fully populated.
358                            self._ignore_ranges[file][check].append((line_number, float("inf")))
359                        else:
360                            logger.error(
361                                f"Consecutive slither-disable-starts without slither-disable-end in {file}#{line_number}"
362                            )
363                            return
364
365            if end_match:
366                ignored = end_match[0].split(",")
367                if ignored:
368                    for check in ignored:
369                        vals = self._ignore_ranges[file][check]
370                        if len(vals) == 0 or vals[-1][1] != float("inf"):
371                            logger.error(
372                                f"slither-disable-end without slither-disable-start in {file}#{line_number}"
373                            )
374                            return
375                        self._ignore_ranges[file][check][-1] = (vals[-1][0], line_number)
376
377            line_number += 1
378
379    def has_ignore_comment(self, r: Dict) -> bool:
380        """
381        Check if the result has an ignore comment in the file or on the preceding line, in which
382        case, it is not valid
383        """
384        if not self.crytic_compile:
385            return False
386        mapping_elements_with_lines = (
387            (
388                posixpath.normpath(elem["source_mapping"]["filename_absolute"]),
389                elem["source_mapping"]["lines"],
390            )
391            for elem in r["elements"]
392            if "source_mapping" in elem
393            and "filename_absolute" in elem["source_mapping"]
394            and "lines" in elem["source_mapping"]
395            and len(elem["source_mapping"]["lines"]) > 0
396        )
397
398        for file, lines in mapping_elements_with_lines:
399
400            # Check if result is within an ignored range.
401            ignore_ranges = self._ignore_ranges[file][r["check"]] + self._ignore_ranges[file]["all"]
402            for start, end in ignore_ranges:
403                # The full check must be within the ignore range to be ignored.
404                if start < lines[0] and end > lines[-1]:
405                    return True
406
407            # Check for next-line matchers.
408            ignore_line_index = min(lines) - 1
409            ignore_line_text = self.crytic_compile.get_code_from_line(file, ignore_line_index)
410            if ignore_line_text:
411                match = re.findall(
412                    r"^\s*//\s*slither-disable-next-line\s*([a-zA-Z0-9_,-]*)",
413                    ignore_line_text.decode("utf8"),
414                )
415                if match:
416                    ignored = match[0].split(",")
417                    if ignored and ("all" in ignored or any(r["check"] == c for c in ignored)):
418                        return True
419
420        return False
421
422    def valid_result(self, r: Dict) -> bool:
423        """
424        Check if the result is valid
425        A result is invalid if:
426            - All its source paths belong to the source path filtered
427            - Or a similar result was reported and saved during a previous run
428            - The --exclude-dependencies flag is set and results are only related to dependencies
429            - There is an ignore comment on the preceding line or in the file
430        """
431
432        # Remove duplicate due to the multiple compilation support
433        if r["id"] in self._currently_seen_resuts:
434            return False
435        self._currently_seen_resuts.add(r["id"])
436
437        source_mapping_elements = [
438            elem["source_mapping"].get("filename_absolute", "unknown")
439            for elem in r["elements"]
440            if "source_mapping" in elem
441        ]
442
443        # Use POSIX-style paths so that filter_paths|include_paths works across different
444        # OSes. Convert to a list so elements don't get consumed and are lost
445        # while evaluating the first pattern
446        source_mapping_elements = list(
447            map(lambda x: pathlib.Path(x).resolve().as_posix() if x else x, source_mapping_elements)
448        )
449        (matching, paths, msg_err) = (
450            (True, self._paths_to_include, "--include-paths")
451            if self._paths_to_include
452            else (False, self._paths_to_filter, "--filter-paths")
453        )
454
455        for path in paths:
456            try:
457                if any(
458                    bool(re.search(_relative_path_format(path), src_mapping))
459                    for src_mapping in source_mapping_elements
460                ):
461                    matching = not matching
462                    break
463            except re.error:
464                logger.error(
465                    f"Incorrect regular expression for {msg_err} {path}."
466                    "\nSlither supports the Python re format"
467                    ": https://docs.python.org/3/library/re.html"
468                )
469
470        if r["elements"] and matching:
471            return False
472
473        if self._show_ignored_findings:
474            return True
475        if self.has_ignore_comment(r):
476            return False
477        if r["id"] in self._previous_results_ids:
478            return False
479        if r["elements"] and self._exclude_dependencies:
480            if all(element["source_mapping"]["is_dependency"] for element in r["elements"]):
481                return False
482        # Conserve previous result filtering. This is conserved for compatibility, but is meant to be removed
483        if r["description"] in [pr["description"] for pr in self._previous_results]:
484            return False
485
486        return True
487
488    def load_previous_results(self) -> None:
489        self.load_previous_results_from_sarif()
490
491        filename = self._previous_results_filename
492        try:
493            if os.path.isfile(filename):
494                with open(filename, encoding="utf8") as f:
495                    self._previous_results = json.load(f)
496                    if self._previous_results:
497                        for r in self._previous_results:
498                            if "id" in r:
499                                self._previous_results_ids.add(r["id"])
500
501        except json.decoder.JSONDecodeError:
502            logger.error(red(f"Impossible to decode {filename}. Consider removing the file"))
503
504    def load_previous_results_from_sarif(self) -> None:
505        sarif = pathlib.Path(self.sarif_input)
506        triage = pathlib.Path(self.sarif_triage)
507
508        if not sarif.exists():
509            return
510        if not triage.exists():
511            return
512
513        triaged = read_triage_info(sarif, triage)
514
515        for id_triaged in triaged:
516            self._previous_results_ids.add(id_triaged)
517
518    def write_results_to_hide(self) -> None:
519        if not self._results_to_hide:
520            return
521        filename = self._previous_results_filename
522        with open(filename, "w", encoding="utf8") as f:
523            results = self._results_to_hide + self._previous_results
524            json.dump(results, f)
525
526    def save_results_to_hide(self, results: List[Dict]) -> None:
527        self._results_to_hide += results
528
529    def add_path_to_filter(self, path: str):
530        """
531        Add path to filter
532        Path are used through direct comparison (no regex)
533        """
534        self._paths_to_filter.add(path)
535
536    def add_path_to_include(self, path: str):
537        """
538        Add path to include
539        Path are used through direct comparison (no regex)
540        """
541        self._paths_to_include.add(path)
542
543    # endregion
544    ###################################################################################
545    ###################################################################################
546    # region Crytic compile
547    ###################################################################################
548    ###################################################################################
549
550    @property
551    def crytic_compile(self) -> CryticCompile:
552        return self._crytic_compile  # type: ignore
553
554    # endregion
555    ###################################################################################
556    ###################################################################################
557    # region Format
558    ###################################################################################
559    ###################################################################################
560
561    @property
562    def generate_patches(self) -> bool:
563        return self._generate_patches
564
565    @generate_patches.setter
566    def generate_patches(self, p: bool):
567        self._generate_patches = p
568
569    # endregion
570    ###################################################################################
571    ###################################################################################
572    # region Internals
573    ###################################################################################
574    ###################################################################################
575
576    @property
577    def disallow_partial(self) -> bool:
578        """
579        Return true if partial analyses are disallowed
580        For example, codebase with duplicate names will lead to partial analyses
581
582        :return:
583        """
584        return self._disallow_partial
585
586    @property
587    def skip_assembly(self) -> bool:
588        return self._skip_assembly
589
590    @property
591    def show_ignore_findings(self) -> bool:
592        return self._show_ignored_findings
593
594    # endregion

Slither static analyzer

sarif_input: str
sarif_triage: str
line_prefix: str
no_fail
skip_data_dependency
compilation_units: List[slither.core.compilation_unit.SlitherCompilationUnit]
108    @property
109    def compilation_units(self) -> List[SlitherCompilationUnit]:
110        return list(self._compilation_units)
def add_compilation_unit( self, compilation_unit: slither.core.compilation_unit.SlitherCompilationUnit):
112    def add_compilation_unit(self, compilation_unit: SlitherCompilationUnit):
113        self._compilation_units.append(compilation_unit)
contracts: List[slither.core.declarations.contract.Contract]
122    @property
123    def contracts(self) -> List[Contract]:
124        if not self._contracts:
125            all_contracts = [
126                compilation_unit.contracts for compilation_unit in self._compilation_units
127            ]
128            self._contracts = [item for sublist in all_contracts for item in sublist]
129        return self._contracts
contracts_derived: List[slither.core.declarations.contract.Contract]
131    @property
132    def contracts_derived(self) -> List[Contract]:
133        if not self._contracts_derived:
134            all_contracts = [
135                compilation_unit.contracts_derived for compilation_unit in self._compilation_units
136            ]
137            self._contracts_derived = [item for sublist in all_contracts for item in sublist]
138        return self._contracts_derived
def get_contract_from_name( self, contract_name: Union[str, slither.slithir.variables.constant.Constant]) -> List[slither.core.declarations.contract.Contract]:
140    def get_contract_from_name(self, contract_name: Union[str, Constant]) -> List[Contract]:
141        """
142            Return a contract from a name
143        Args:
144            contract_name (str): name of the contract
145        Returns:
146            Contract
147        """
148        contracts = []
149        for compilation_unit in self._compilation_units:
150            contracts += compilation_unit.get_contract_from_name(contract_name)
151        return contracts

Return a contract from a name Args: contract_name (str): name of the contract Returns: Contract

source_code: Dict[str, str]
159    @property
160    def source_code(self) -> Dict[str, str]:
161        """{filename: source_code (str)}: source code"""
162        return self._raw_source_code

{filename: source_code (str)}: source code

filename: Union[str, NoneType]
164    @property
165    def filename(self) -> Optional[str]:
166        """str: Filename."""
167        return self._filename

str: Filename.

def add_source_code(self, path: str) -> None:
173    def add_source_code(self, path: str) -> None:
174        """
175        :param path:
176        :return:
177        """
178        if self.crytic_compile and path in self.crytic_compile.src_content:
179            self.source_code[path] = self.crytic_compile.src_content[path]
180        else:
181            with open(path, encoding="utf8", newline="") as f:
182                self.source_code[path] = f.read()
183
184        self.parse_ignore_comments(path)
Parameters
  • path:
Returns
markdown_root: str
186    @property
187    def markdown_root(self) -> str:
188        return self._markdown_root
def print_functions(self, d: str):
190    def print_functions(self, d: str):
191        """
192        Export all the functions to dot files
193        """
194        for compilation_unit in self._compilation_units:
195            for c in compilation_unit.contracts:
196                for f in c.functions:
197                    f.cfg_to_dot(os.path.join(d, f"{c.name}.{f.name}.dot"))

Export all the functions to dot files

def offset_to_objects( self, filename_str: str, offset: int) -> Set[slither.core.source_mapping.source_mapping.SourceMapping]:
199    def offset_to_objects(self, filename_str: str, offset: int) -> Set[SourceMapping]:
200        if self._offset_to_objects is None:
201            self._compute_offsets_to_ref_impl_decl()
202        filename: Filename = self.crytic_compile.filename_lookup(filename_str)
203        return self._offset_to_objects[filename][offset]
def offset_to_references( self, filename_str: str, offset: int) -> Set[slither.core.source_mapping.source_mapping.Source]:
312    def offset_to_references(self, filename_str: str, offset: int) -> Set[Source]:
313        if self._offset_to_references is None:
314            self._compute_offsets_to_ref_impl_decl()
315        filename: Filename = self.crytic_compile.filename_lookup(filename_str)
316        return self._offset_to_references[filename][offset]
def offset_to_implementations( self, filename_str: str, offset: int) -> Set[slither.core.source_mapping.source_mapping.Source]:
318    def offset_to_implementations(self, filename_str: str, offset: int) -> Set[Source]:
319        if self._offset_to_implementations is None:
320            self._compute_offsets_to_ref_impl_decl()
321        filename: Filename = self.crytic_compile.filename_lookup(filename_str)
322        return self._offset_to_implementations[filename][offset]
def offset_to_definitions( self, filename_str: str, offset: int) -> Set[slither.core.source_mapping.source_mapping.Source]:
324    def offset_to_definitions(self, filename_str: str, offset: int) -> Set[Source]:
325        if self._offset_to_definitions is None:
326            self._compute_offsets_to_ref_impl_decl()
327        filename: Filename = self.crytic_compile.filename_lookup(filename_str)
328        return self._offset_to_definitions[filename][offset]
def parse_ignore_comments(self, file: str) -> None:
337    def parse_ignore_comments(self, file: str) -> None:
338        # The first time we check a file, find all start/end ignore comments and memoize them.
339        line_number = 1
340        while True:
341
342            line_text = self.crytic_compile.get_code_from_line(file, line_number)
343            if line_text is None:
344                break
345
346            start_regex = r"^\s*//\s*slither-disable-start\s*([a-zA-Z0-9_,-]*)"
347            end_regex = r"^\s*//\s*slither-disable-end\s*([a-zA-Z0-9_,-]*)"
348            start_match = re.findall(start_regex, line_text.decode("utf8"))
349            end_match = re.findall(end_regex, line_text.decode("utf8"))
350
351            if start_match:
352                ignored = start_match[0].split(",")
353                if ignored:
354                    for check in ignored:
355                        vals = self._ignore_ranges[file][check]
356                        if len(vals) == 0 or vals[-1][1] != float("inf"):
357                            # First item in the array, or the prior item is fully populated.
358                            self._ignore_ranges[file][check].append((line_number, float("inf")))
359                        else:
360                            logger.error(
361                                f"Consecutive slither-disable-starts without slither-disable-end in {file}#{line_number}"
362                            )
363                            return
364
365            if end_match:
366                ignored = end_match[0].split(",")
367                if ignored:
368                    for check in ignored:
369                        vals = self._ignore_ranges[file][check]
370                        if len(vals) == 0 or vals[-1][1] != float("inf"):
371                            logger.error(
372                                f"slither-disable-end without slither-disable-start in {file}#{line_number}"
373                            )
374                            return
375                        self._ignore_ranges[file][check][-1] = (vals[-1][0], line_number)
376
377            line_number += 1
def has_ignore_comment(self, r: Dict) -> bool:
379    def has_ignore_comment(self, r: Dict) -> bool:
380        """
381        Check if the result has an ignore comment in the file or on the preceding line, in which
382        case, it is not valid
383        """
384        if not self.crytic_compile:
385            return False
386        mapping_elements_with_lines = (
387            (
388                posixpath.normpath(elem["source_mapping"]["filename_absolute"]),
389                elem["source_mapping"]["lines"],
390            )
391            for elem in r["elements"]
392            if "source_mapping" in elem
393            and "filename_absolute" in elem["source_mapping"]
394            and "lines" in elem["source_mapping"]
395            and len(elem["source_mapping"]["lines"]) > 0
396        )
397
398        for file, lines in mapping_elements_with_lines:
399
400            # Check if result is within an ignored range.
401            ignore_ranges = self._ignore_ranges[file][r["check"]] + self._ignore_ranges[file]["all"]
402            for start, end in ignore_ranges:
403                # The full check must be within the ignore range to be ignored.
404                if start < lines[0] and end > lines[-1]:
405                    return True
406
407            # Check for next-line matchers.
408            ignore_line_index = min(lines) - 1
409            ignore_line_text = self.crytic_compile.get_code_from_line(file, ignore_line_index)
410            if ignore_line_text:
411                match = re.findall(
412                    r"^\s*//\s*slither-disable-next-line\s*([a-zA-Z0-9_,-]*)",
413                    ignore_line_text.decode("utf8"),
414                )
415                if match:
416                    ignored = match[0].split(",")
417                    if ignored and ("all" in ignored or any(r["check"] == c for c in ignored)):
418                        return True
419
420        return False

Check if the result has an ignore comment in the file or on the preceding line, in which case, it is not valid

def valid_result(self, r: Dict) -> bool:
422    def valid_result(self, r: Dict) -> bool:
423        """
424        Check if the result is valid
425        A result is invalid if:
426            - All its source paths belong to the source path filtered
427            - Or a similar result was reported and saved during a previous run
428            - The --exclude-dependencies flag is set and results are only related to dependencies
429            - There is an ignore comment on the preceding line or in the file
430        """
431
432        # Remove duplicate due to the multiple compilation support
433        if r["id"] in self._currently_seen_resuts:
434            return False
435        self._currently_seen_resuts.add(r["id"])
436
437        source_mapping_elements = [
438            elem["source_mapping"].get("filename_absolute", "unknown")
439            for elem in r["elements"]
440            if "source_mapping" in elem
441        ]
442
443        # Use POSIX-style paths so that filter_paths|include_paths works across different
444        # OSes. Convert to a list so elements don't get consumed and are lost
445        # while evaluating the first pattern
446        source_mapping_elements = list(
447            map(lambda x: pathlib.Path(x).resolve().as_posix() if x else x, source_mapping_elements)
448        )
449        (matching, paths, msg_err) = (
450            (True, self._paths_to_include, "--include-paths")
451            if self._paths_to_include
452            else (False, self._paths_to_filter, "--filter-paths")
453        )
454
455        for path in paths:
456            try:
457                if any(
458                    bool(re.search(_relative_path_format(path), src_mapping))
459                    for src_mapping in source_mapping_elements
460                ):
461                    matching = not matching
462                    break
463            except re.error:
464                logger.error(
465                    f"Incorrect regular expression for {msg_err} {path}."
466                    "\nSlither supports the Python re format"
467                    ": https://docs.python.org/3/library/re.html"
468                )
469
470        if r["elements"] and matching:
471            return False
472
473        if self._show_ignored_findings:
474            return True
475        if self.has_ignore_comment(r):
476            return False
477        if r["id"] in self._previous_results_ids:
478            return False
479        if r["elements"] and self._exclude_dependencies:
480            if all(element["source_mapping"]["is_dependency"] for element in r["elements"]):
481                return False
482        # Conserve previous result filtering. This is conserved for compatibility, but is meant to be removed
483        if r["description"] in [pr["description"] for pr in self._previous_results]:
484            return False
485
486        return True

Check if the result is valid A result is invalid if: - All its source paths belong to the source path filtered - Or a similar result was reported and saved during a previous run - The --exclude-dependencies flag is set and results are only related to dependencies - There is an ignore comment on the preceding line or in the file

def load_previous_results(self) -> None:
488    def load_previous_results(self) -> None:
489        self.load_previous_results_from_sarif()
490
491        filename = self._previous_results_filename
492        try:
493            if os.path.isfile(filename):
494                with open(filename, encoding="utf8") as f:
495                    self._previous_results = json.load(f)
496                    if self._previous_results:
497                        for r in self._previous_results:
498                            if "id" in r:
499                                self._previous_results_ids.add(r["id"])
500
501        except json.decoder.JSONDecodeError:
502            logger.error(red(f"Impossible to decode {filename}. Consider removing the file"))
def load_previous_results_from_sarif(self) -> None:
504    def load_previous_results_from_sarif(self) -> None:
505        sarif = pathlib.Path(self.sarif_input)
506        triage = pathlib.Path(self.sarif_triage)
507
508        if not sarif.exists():
509            return
510        if not triage.exists():
511            return
512
513        triaged = read_triage_info(sarif, triage)
514
515        for id_triaged in triaged:
516            self._previous_results_ids.add(id_triaged)
def write_results_to_hide(self) -> None:
518    def write_results_to_hide(self) -> None:
519        if not self._results_to_hide:
520            return
521        filename = self._previous_results_filename
522        with open(filename, "w", encoding="utf8") as f:
523            results = self._results_to_hide + self._previous_results
524            json.dump(results, f)
def save_results_to_hide(self, results: List[Dict]) -> None:
526    def save_results_to_hide(self, results: List[Dict]) -> None:
527        self._results_to_hide += results
def add_path_to_filter(self, path: str):
529    def add_path_to_filter(self, path: str):
530        """
531        Add path to filter
532        Path are used through direct comparison (no regex)
533        """
534        self._paths_to_filter.add(path)

Add path to filter Path are used through direct comparison (no regex)

def add_path_to_include(self, path: str):
536    def add_path_to_include(self, path: str):
537        """
538        Add path to include
539        Path are used through direct comparison (no regex)
540        """
541        self._paths_to_include.add(path)

Add path to include Path are used through direct comparison (no regex)

crytic_compile: crytic_compile.crytic_compile.CryticCompile
550    @property
551    def crytic_compile(self) -> CryticCompile:
552        return self._crytic_compile  # type: ignore
generate_patches: bool
561    @property
562    def generate_patches(self) -> bool:
563        return self._generate_patches
disallow_partial: bool
576    @property
577    def disallow_partial(self) -> bool:
578        """
579        Return true if partial analyses are disallowed
580        For example, codebase with duplicate names will lead to partial analyses
581
582        :return:
583        """
584        return self._disallow_partial

Return true if partial analyses are disallowed For example, codebase with duplicate names will lead to partial analyses

Returns
skip_assembly: bool
586    @property
587    def skip_assembly(self) -> bool:
588        return self._skip_assembly
show_ignore_findings: bool
590    @property
591    def show_ignore_findings(self) -> bool:
592        return self._show_ignored_findings