Skip to content

Commit

Permalink
Deep clone fix, Main decluttering
Browse files Browse the repository at this point in the history
  • Loading branch information
forefy committed Mar 8, 2024
1 parent 9abe6e5 commit 638bd90
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 153 deletions.
172 changes: 27 additions & 145 deletions eburger/main.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,26 @@
import json
import os
from pathlib import Path
import shutil
import sys
from pathlib import Path

import eburger.settings as settings
from eburger.serializer import parse_solidity_ast, reduce_json
from eburger.utils.cli_args import args
from eburger.utils.compilers import compile_foundry, compile_hardhat, compile_solc
from eburger.utils.filesystem import (
create_directory_if_not_exists,
create_or_empty_directory,
find_and_read_sol_file,
find_recursive_files_by_patterns,
get_foundry_ast_json,
get_hardhat_ast_json,
get_solidity_version_from_file,
roughly_check_valid_file_path_name,
select_project,
)
from eburger.utils.helpers import (
construct_solc_cmdline,
get_eburger_version,
get_filename_from_path,
is_valid_json,
run_command,
)
from eburger.utils.installers import (
construct_sourceable_nvm_string,
install_foundry_if_not_found,
install_hardhat_if_not_found,
set_solc_compiler_version,
)
from eburger.utils.logger import log
import eburger.settings as settings
from eburger.serializer import parse_solidity_ast, reduce_json
from eburger.utils.outputs import (
calculate_nsloc,
draw_nsloc_table,
Expand All @@ -46,15 +36,28 @@ def main():
print(get_eburger_version())
sys.exit(0)

if not args.solidity_file_or_folder and not args.ast_json_file:
if not args.solidity_file_or_folder:
args.solidity_file_or_folder = "."

log("debug", f"Project path: {args.solidity_file_or_folder}")

create_directory_if_not_exists(settings.outputs_dir)
path_type = None

if args.solidity_file_or_folder:
if args.ast_json_file:
filename = args.ast_json_file
filename = filename.replace(".json", "") # Clean possible file extension
filename = filename.replace(
".eburger/", ""
) # Protection against analysis of files inside the output path
with open(args.ast_json_file, "r") as f:
ast_json = json.load(f)
ast_json, src_paths = reduce_json(ast_json)
output_path = settings.outputs_dir / f"{filename}.json"

save_as_json(output_path, ast_json)

elif args.solidity_file_or_folder:
if os.path.isfile(args.solidity_file_or_folder):
log("debug", f"Path type: file")
path_type = "file"
Expand Down Expand Up @@ -111,161 +114,40 @@ def main():
)
sys.exit(0)

elif args.ast_json_file:
filename = args.ast_json_file
filename = filename.replace(".json", "") # Clean possible file extension
filename = filename.replace(
".eburger/", ""
) # Protection against analysis of files inside the output path
with open(args.ast_json_file, "r") as f:
ast_json = json.load(f)
ast_json, src_paths = reduce_json(ast_json)
output_path = settings.outputs_dir / f"{filename}.json"

save_as_json(output_path, ast_json)

# NSLOC only mode
if args.nsloc:
draw_nsloc_table()

# Compilation
if path_type is not None:
# Foundry compilation flow
if path_type == "foundry":
log("info", "Foundry project detected, compiling using forge.")
_, forge_full_path_binary_found = install_foundry_if_not_found()

if args.solc_remappings:
log("warning", "Ignoring the -r option in foundry based projects.")

# Call foundry's full path if necessary, otherwise use the bins available through PATH
forge_clean_command = "forge clean"
if forge_full_path_binary_found:
forge_clean_command = (
f"{os.environ.get('HOME')}/.foundry/bin/{forge_clean_command}"
)

run_command(forge_clean_command, directory=args.solidity_file_or_folder)
forge_out_dir = settings.outputs_dir / "forge-output"
create_or_empty_directory(forge_out_dir)

foundry_excluded_dirs = " ".join(
[item for item in settings.excluded_dirs if item != "lib"]
)
# Call foundry's full path if necessary, otherwise use the bins available through PATH
forge_build_command = f"forge build --force --skip {foundry_excluded_dirs} --build-info --build-info-path {forge_out_dir}"
if forge_full_path_binary_found:
forge_build_command = (
f"{os.environ.get('HOME')}/.foundry/bin/{forge_build_command}"
)

run_command(
forge_build_command,
directory=args.solidity_file_or_folder,
live_output=args.debug,
output_filename, ast_json, filename, src_paths = compile_foundry(
forge_full_path_binary_found
)
sample_file_path = find_and_read_sol_file(args.solidity_file_or_folder)
filename, output_filename = get_filename_from_path(sample_file_path)
ast_json = get_foundry_ast_json(forge_out_dir)
ast_json, src_paths = reduce_json(ast_json)
save_as_json(output_filename, ast_json)

# Hardhat compilation flow
elif path_type == "hardhat":
log("info", "Hardhat project detected, compiling using hardhat.")
install_hardhat_if_not_found()

if args.solc_remappings:
log("warning", "Ignoring the -r option in hardhat based projects.")

# try runing npx normally, as a fallback try the construct_sourceable_nvm_string method
# if a user hadn't got npx installed / or it's not on path (meaning it was installed in same run as the analysis)
# it still needs the fallback option
try:
run_command(f"npx hardhat clean", directory=settings.project_root)
run_command(
f"npx hardhat compile --force",
directory=settings.project_root,
live_output=args.debug,
)
except FileNotFoundError:
run_command(
construct_sourceable_nvm_string("npx hardhat clean"),
directory=settings.project_root,
)
run_command(
construct_sourceable_nvm_string("npx hardhat compile --force"),
directory=settings.project_root,
live_output=args.debug,
)

# Copy compilation results to .eburger
expected_hardhat_outfiles = os.path.join(
args.solidity_file_or_folder, "artifacts", "build-info"
)
if not os.path.isdir(expected_hardhat_outfiles):
log(
"error",
f"Hardhat's compilation files were not found in expected location {expected_hardhat_outfiles}",
)
hardhat_out_dir = settings.outputs_dir / "hardhat-output"
if os.path.exists(hardhat_out_dir):
shutil.rmtree(hardhat_out_dir)
shutil.copytree(expected_hardhat_outfiles, hardhat_out_dir)

sample_file_path = find_and_read_sol_file(args.solidity_file_or_folder)
filename, output_filename = get_filename_from_path(sample_file_path)

ast_json = get_hardhat_ast_json(hardhat_out_dir)
ast_json, src_paths = reduce_json(ast_json)
output_filename, ast_json, filename, src_paths = compile_hardhat()
save_as_json(output_filename, ast_json)

# solc compilation flow
elif path_type in ["file", "folder"]:
try:
run_command("solc --version")
run_command("solc-select versions")
except FileNotFoundError:
log(
"info",
"Please ensure solc and solc-select are installed and availble globally, and run again.",
)
sys.exit(0)

sample_file_path = args.solidity_file_or_folder
compilation_source_path = args.solidity_file_or_folder

if path_type == "folder":
sample_file_path = find_and_read_sol_file(args.solidity_file_or_folder)

filename, output_filename = get_filename_from_path(sample_file_path)
output_filename, ast_json, filename, src_paths, solc_compile_res_parsed = (
compile_solc(path_type)
)

if args.solc_compiler_version:
solc_required_version = args.solc_compiler_version
else:
solc_required_version = get_solidity_version_from_file(sample_file_path)
set_solc_compiler_version(solc_required_version)
solc_cmdline = construct_solc_cmdline(path_type, compilation_source_path)
if solc_cmdline is None:
log("error", "Error constructing solc command line")

solc_compile_res, _ = run_command(solc_cmdline, live_output=args.debug)

# We continue as long as solc compiled something
if not is_valid_json(solc_compile_res):
error_string = "Locally installed solc errored out trying to compile the contract. Please review comiler warnings above"
if not args.solc_remappings:
error_string += (
"or see if library remappings (using the -r option) are needed"
)
if not args.solc_compiler_version:
error_string += ", or try specifing the solidity compiler version (using the -s option)"
error_string += "."
log(
"error",
error_string,
)
solc_compile_res_parsed = json.loads("".join(solc_compile_res))
ast_json, src_paths = reduce_json(solc_compile_res_parsed)
save_as_json(output_filename, solc_compile_res_parsed)

# Parse AST
Expand Down
10 changes: 10 additions & 0 deletions eburger/template_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import copy
import re
from typing import Union

Expand Down Expand Up @@ -211,3 +212,12 @@ def function_def_has_following_check_statements(

# If we go through all statements following the target node without finding a validation, return False
return False


def deep_clone_node(node: dict):
"""
Deep clone an AST node for template level manipulations.
:param node: AST Node to clone.
"""
return copy.deepcopy(node)
9 changes: 5 additions & 4 deletions eburger/templates/missing_reentrancy_guard.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: 1.0.5
version: 1.0.6
name: "Missing Reentracy Guard"
severity: "Low"
precision: "Low"
Expand Down Expand Up @@ -35,10 +35,11 @@ python: |
function_node_body = function_node.get("body", {})
# Ignore .call usage within the first entries of the function (where reentrancy doesn't affect anything)
if function_node_body.get("statements"):
function_node_body["statements"] = function_node_body["statements"][1:]
function_node_body_clone = deep_clone_node(function_node_body)
if function_node_body_clone.get("statements"):
function_node_body_clone["statements"] = function_node_body["statements"][1:]
call_nodes = get_nodes_by_signature(function_node_body, "function (bytes memory) payable returns (bool,bytes memory)")
call_nodes = get_nodes_by_signature(function_node_body_clone, "function (bytes memory) payable returns (bool,bytes memory)")
if call_nodes:
results.append(function_node)
Loading

0 comments on commit 638bd90

Please sign in to comment.