/
run.py
219 lines (193 loc) · 7.64 KB
/
run.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import logging
import os
import traceback
from enum import Enum
from pathlib import Path
import click
from mythx_cli.fuzz.ide.brownie import BrownieJob
from mythx_cli.fuzz.ide.hardhat import HardhatJob
from .exceptions import BadStatusCode, RPCCallError
from .faas import FaasClient
from .rpc import RPCClient
LOGGER = logging.getLogger("mythx-cli")
headers = {"Content-Type": "application/json"}
time_limit_seconds = 3000
class IDE(Enum):
BROWNIE = "brownie"
HARDHAT = "hardhat"
TRUFFLE = "truffle"
SOLIDITY = "solidity"
def determine_ide() -> IDE:
root_dir = Path.cwd().absolute()
files = list(os.walk(root_dir))[0][2]
if "brownie-config.yaml" in files:
return IDE.BROWNIE
if "hardhat.config.ts" in files:
return IDE.HARDHAT
if "hardhat.config.js" in files:
return IDE.HARDHAT
if "truffle-config.js" in files:
return IDE.TRUFFLE
return IDE.SOLIDITY
@click.command("run")
@click.argument("target", default=None, nargs=-1, required=False)
@click.option(
"-a", "--address", type=click.STRING, help="Address of the main contract to analyze"
)
@click.option(
"-m",
"--more-addresses",
type=click.STRING,
help="Addresses of other contracts to analyze, separated by commas",
)
@click.option(
"-c",
"--corpus-target",
type=click.STRING,
help="Project UUID, Campaign UUID or Corpus UUID to reuse the corpus from. "
"In case of a project, corpus from the project's latest submitted campaign will be used",
default=None,
required=False,
)
@click.option(
"-s",
"--map-to-original-source",
is_flag=True,
default=False,
help="Map the analyses results to the original source code, instead of the instrumented one. "
"This is meant to be used with Scribble.",
)
@click.option(
"--dry-run",
is_flag=True,
default=False,
help="Outputs the data to be sent to the FaaS API without making the request.",
)
@click.pass_obj
def fuzz_run(ctx, address, more_addresses, corpus_target, map_to_original_source, dry_run, target):
# read YAML config params from ctx dict, e.g. ganache rpc url
# Introduce a separate `fuzz` section in the YAML file
# construct seed state from ganache
# construct the FaaS campaign object
# helpful method: mythx_cli/analyze/solidity.py:SolidityJob.generate_payloads
# NOTE: This currently patches link placeholders in the creation
# bytecode with the zero address. If we need to submit bytecode from
# solc compilation, we need to find a way to replace these with the Ganache
# instance's addresses. Preferably we pull all of this data from Ganache
# itself and just enrich the payload with source and AST data from the
# SolidityJob payload list
# submit the FaaS payload, do error handling
# print FaaS dashboard url pointing to campaign
analyze_config = ctx.get("fuzz")
default_config = {
"rpc_url": "http://localhost:7545",
"faas_url": "http://localhost:8080",
"harvey_num_cores": 2,
"campaign_name_prefix": "untitled",
"map_to_original_source": False,
}
config_options = analyze_config.keys()
# Mandatory config parameters verification
if "build_directory" not in config_options:
raise click.exceptions.UsageError(
"build_directory not found on .mythx.yml config file"
"\nYou need to provide your project's build directory in the .mythx.yml config file"
)
if "deployed_contract_address" not in config_options:
raise click.exceptions.UsageError(
"deployed_contract_address not found on .mythx.yml config file."
"\nYou need to provide the address where your main contract is deployed on the .mythx.yml"
)
if not target and "targets" not in config_options:
raise click.exceptions.UsageError(
"Target not provided. You need to provide a target as the last parameter of the fuzz run command."
"\nYou can also set the target on the `fuzz` key of your .mythx.yml config file."
"\nSee https://mythx-cli.readthedocs.io/en/latest/advanced-usage.html#configuration-using-mythx-yml"
" for more information."
)
if not target:
target = analyze_config["targets"]
if not map_to_original_source:
map_to_original_source = (
analyze_config["map_to_original_source"]
if "map_to_original_source" in config_options
else default_config["map_to_original_source"]
)
# Optional config parameters
# Here we parse the config parameters from the config file and use defaults for non available values
contract_address = analyze_config["deployed_contract_address"]
rpc_url = (
analyze_config["rpc_url"]
if "rpc_url" in config_options
else default_config["rpc_url"]
)
faas_url = (
analyze_config["faas_url"]
if "faas_url" in config_options
else default_config["faas_url"]
)
number_of_cores = (
analyze_config["number_of_cores"]
if "number_of_cores" in config_options
else default_config["harvey_num_cores"]
)
campaign_name_prefix = (
analyze_config["campaign_name_prefix"]
if "campaign_name_prefix" in config_options
else default_config["campaign_name_prefix"]
)
if more_addresses is None:
other_addresses = []
else:
other_addresses = more_addresses.split(",")
_corpus_target = corpus_target or analyze_config.get("corpus_target", None)
rpc_client = RPCClient(rpc_url, number_of_cores)
if not _corpus_target:
try:
contract_code_response = rpc_client.contract_exists(contract_address)
except RPCCallError as e:
raise click.exceptions.UsageError(f"RPC endpoint." f"\n{e}")
if not contract_code_response:
LOGGER.warning(f"Contract code not found")
raise click.exceptions.ClickException(
f"Unable to find a contract deployed at {contract_address}."
)
seed_state = rpc_client.get_seed_state(
contract_address, other_addresses, _corpus_target
)
ide = determine_ide()
if ide == IDE.BROWNIE:
artifacts = BrownieJob(target, analyze_config["build_directory"], map_to_original_source=map_to_original_source)
artifacts.generate_payload()
elif ide == IDE.HARDHAT:
artifacts = HardhatJob(target, analyze_config["build_directory"], map_to_original_source=map_to_original_source)
artifacts.generate_payload()
elif ide == IDE.TRUFFLE:
raise click.exceptions.UsageError(
f"Projects using Truffle IDE is not supported right now"
)
else:
raise click.exceptions.UsageError(
f"Projects using plain solidity files is not supported right now"
)
faas_client = FaasClient(
faas_url=faas_url, campaign_name_prefix=campaign_name_prefix, project_type=ide
)
try:
campaign_id = faas_client.create_faas_campaign(
campaign_data=artifacts, seed_state=seed_state, dry_run=dry_run
)
click.echo(
"You can view campaign here: " + faas_url + "/campaigns/" + str(campaign_id)
)
except BadStatusCode as e:
raise click.exceptions.UsageError(
f"Campaign submission error. Detail - {e.detail}"
)
except Exception as e:
LOGGER.warning(
f"Could not submit campaign to the FaaS\n{traceback.format_exc()}"
)
raise click.exceptions.UsageError(
f"Unable to submit the campaign to the faas. Are you sure the service is running on {faas_url} ?"
)