Skip to content

Commit

Permalink
Filter out files before a too deep analysis.
Browse files Browse the repository at this point in the history
  • Loading branch information
DarkaMaul committed Apr 23, 2024
1 parent 9e223dc commit 6c8ee23
Show file tree
Hide file tree
Showing 11 changed files with 165 additions and 9 deletions.
20 changes: 20 additions & 0 deletions slither/core/scope/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from slither.core.declarations.structure_top_level import StructureTopLevel
from slither.core.solidity_types import TypeAlias
from slither.core.variables.top_level_variable import TopLevelVariable
from slither.exceptions import SlitherError
from slither.slithir.variables import Constant


Expand Down Expand Up @@ -93,6 +94,25 @@ def add_accessible_scopes(self) -> bool: # pylint: disable=too-many-branches

return learn_something

def get_all_files(self, seen: Set["FileScope"]) -> Set[Filename]:
"""Recursively find all files considered in this FileScope.
The parameter seen here is to prevent circular import from generating an infinite loop.
"""

if self in seen:
return set()

seen.add(self)
if len(seen) > 1_000_000:
raise SlitherError("Unable to analyze all files considered in this FileScope.")

files = {self.filename}
for file_scope in self.accessible_scopes:
files |= file_scope.get_all_files(seen)

return files

def get_contract_from_name(self, name: Union[str, Constant]) -> Optional[Contract]:
if isinstance(name, Constant):
return self.contracts.get(name.name, None)
Expand Down
40 changes: 31 additions & 9 deletions slither/slither.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import logging
from typing import Union, List, Type, Dict, Optional
from typing import Union, List, Type, Dict, Optional, Set

from crytic_compile import CryticCompile, InvalidCompilation
from crytic_compile.utils.naming import Filename

# pylint: disable= no-name-in-module
from slither.core.compilation_unit import SlitherCompilationUnit
Expand Down Expand Up @@ -37,13 +38,19 @@ def _check_common_things(

def _update_file_scopes(
sol_parser: SlitherCompilationUnitSolc,
): # pylint: disable=too-many-branches
analyzed_files: Set[Filename],
): # pylint: disable=too-many-branches, disable=too-many-locals
"""
Since all definitions in a file are exported by default, including definitions from its (transitive) dependencies,
we can identify all top level items that could possibly be referenced within the file from its exportedSymbols.
It is not as straightforward for user defined types and functions as well as aliasing. See add_accessible_scopes for more details.
"""
candidates = sol_parser.compilation_unit.scopes.values()
# First, lets remove the filtered candidates
candidates = [
scope
for scope in sol_parser.compilation_unit.scopes.values()
if scope.filename in analyzed_files
]
learned_something = False
# Because solc's import allows cycle in the import graph, iterate until we aren't adding new information to the scope.
while True:
Expand Down Expand Up @@ -128,6 +135,11 @@ def __init__(self, target: Union[str, CryticCompile], **kwargs) -> None:

self.no_fail = kwargs.get("no_fail", False)

# Initialize the filtering system
self.filters: List[FilteringRule] = kwargs.get("filters", [])
if any(filter.type == FilteringAction.ALLOW for filter in self.filters):
self.default_action = FilteringAction.REJECT

self._parsers: List[SlitherCompilationUnitSolc] = []
try:
if isinstance(target, CryticCompile):
Expand Down Expand Up @@ -157,7 +169,22 @@ def __init__(self, target: Union[str, CryticCompile], **kwargs) -> None:
sol_parser.parse_top_level_items(ast, path)
self.add_source_code(path)

# Get all the files that are matched by the current set of filters.
# Don't forget considering transitive dependencies
files_to_analyze = set()
for contract in sol_parser._underlying_contract_to_parser:
if self.filter_contract(contract):
continue

files_to_analyze.add(contract.file_scope.filename)
files_to_analyze |= contract.file_scope.get_all_files(set())

for contract in sol_parser._underlying_contract_to_parser:

if contract.file_scope.filename not in files_to_analyze:
logger.debug("Filtering out %s", contract.file_scope.filename)
continue

if contract.name.startswith("SlitherInternalTopLevelContract"):
raise SlitherError(
# region multi-line-string
Expand All @@ -168,7 +195,7 @@ def __init__(self, target: Union[str, CryticCompile], **kwargs) -> None:
sol_parser._contracts_by_id[contract.id] = contract
sol_parser._compilation_unit.add_contract(contract)

_update_file_scopes(sol_parser)
_update_file_scopes(sol_parser, files_to_analyze)

if kwargs.get("generate_patches", False):
self.generate_patches = True
Expand All @@ -178,11 +205,6 @@ def __init__(self, target: Union[str, CryticCompile], **kwargs) -> None:
self._detectors = []
self._printers = []

# Initialize the filtering system
self.filters: List[FilteringRule] = kwargs.get("filters", [])
if any(filter.type == FilteringAction.ALLOW for filter in self.filters):
self.default_action = FilteringAction.REJECT

self._exclude_dependencies = kwargs.get("exclude_dependencies", False)

triage_mode = kwargs.get("triage_mode", False)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Foundry

**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**

This test should be installed using
```shell
forge install --no-commit --no-git foundry-rs/forge-std
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import {B} from "../sub2/B.sol";
import "./E.sol";

contract A is E {
B public b;

constructor(B b_contract) {
b = b_contract;
}
function a() public view {
b.b();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import {B} from "../sub2/B.sol";

contract C {

B.Status public status;

constructor(){

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "./F.sol";

contract E is F{
constructor(){

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "./E.sol";

contract F {
constructor(){

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

contract B {

enum Status {
VALID,
INVALID
}

constructor(){

}
function b() public view {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

contract D {
constructor(){
}
}
29 changes: 29 additions & 0 deletions tests/e2e/filtering/test_filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,3 +382,32 @@ def run_printer(
FilteringAction.REJECT,
)
assert not output ^ {"E.constructor"}


@pytest.mark.skipif(
not foundry_available
or not Path(TEST_DATA_DIR, "test_filtering_analysis/lib/forge-std").exists(),
reason="requires Foundry and project setup",
)
def test_filtering_file_before_parsing():
slither = Slither(
Path(TEST_DATA_DIR, "test_filtering_analysis").as_posix(),
filters=[
FilteringRule(
type=FilteringAction.REJECT,
path=re.compile("sub2/"),
),
],
)

slither.register_printer(DummyPrinter)
printer_output = DummyPrinter.analyze_dummy_output(slither.run_printers().pop())

# We want to not get any results in sub2 but still manage to analyze A that depends on B.
assert not printer_output ^ {
"A.a",
"A.constructor",
"C.constructor",
"E.constructor",
"F.constructor",
}

0 comments on commit 6c8ee23

Please sign in to comment.