Skip to content

Commit 9da78dd

Browse files
committed
contract analisys
1 parent a03a8a0 commit 9da78dd

10 files changed

+1666
-28
lines changed

Dockerfile

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
FROM node:carbon-alpine
22

3+
RUN apk add --update git build-base libgit2-dev make python curl-dev g++ && \
4+
rm -rf /tmp/* /var/cache/apk/*
5+
6+
RUN ln -s /usr/lib/libcurl.so.4 /usr/lib/libcurl-gnutls.so.4
7+
38
WORKDIR /usr/app
49

510
# Install node dependencies - done in a separate step so Docker can cache it.
611
COPY yarn.lock .
712
COPY package.json .
813

9-
RUN yarn install --frozen-lockfile && yarn cache clean
14+
RUN BUILD_ONLY=true yarn install --frozen-lockfile && yarn cache clean
1015

1116
COPY . .
1217

package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"migrate": "npm run compile && sequelize --config sequelizecli.js db:migrate",
1616
"prebuild": "npm run clean && npm run init",
1717
"build": "tsc --sourceMap false --outDir dist --copyFiles",
18-
"compile": "tsc",
18+
"compile": "tsc --sourceMap",
1919
"pretest": "npm run compile && npm run lint",
2020
"test": "tape \"test/**/*.js\" | tap-spec",
2121
"coverage": "nyc --reporter=text --all --include \"src/**/*.js\" npm test",
@@ -38,19 +38,23 @@
3838
"helmet": "^3.13.0",
3939
"joi": "^13.5.2",
4040
"morgan": "^1.9.0",
41+
"nodegit": "^0.24.2",
4142
"nodemon": "^1.17.4",
4243
"pg": "^7.4.3",
4344
"pug": "^2.0.3",
4445
"reflect-metadata": "^0.1.12",
46+
"remix-lib": "^0.4.5",
4547
"sequelize": "^4.38.0",
4648
"sequelize-typescript": "^0.6.6",
49+
"solc": "^0.5.7",
4750
"umzug": "^2.1.0",
4851
"winston": "^3.0.0"
4952
},
5053
"devDependencies": {
5154
"@types/express": "^4.16.0",
5255
"@types/isomorphic-fetch": "^0.0.35",
5356
"@types/node": "^11.13.6",
57+
"@types/nodegit": "^0.24.6",
5458
"@types/supertest": "^2.0.5",
5559
"@types/tape": "^4.2.32",
5660
"coveralls": "^3.0.3",

src/Services/GitMythXConfig.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export interface IGitMythXConfig {
2+
solidityVersion: string;
3+
contracts: IContractConfig[];
4+
}
5+
6+
interface IContractConfig {
7+
name: string;
8+
// relative to giithub repo root
9+
path: string;
10+
}

src/Services/GitRepo.ts

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import {file} from "babel-types";
2+
import * as fs from "fs";
3+
import {Clone, Repository} from "nodegit";
4+
import os from "os";
5+
import path from "path";
6+
import rimraf from "rimraf";
7+
import {IGitMythXConfig} from "./GitMythXConfig";
8+
import logger from "./Logger";
9+
10+
export class GitRepo {
11+
12+
private repo: Repository;
13+
14+
public constructor(
15+
private readonly ownerId: number,
16+
private readonly fullRepoName: string,
17+
private readonly repository: string,
18+
private readonly branch: string,
19+
private readonly ref: string,
20+
private readonly installationToken: string) {
21+
22+
}
23+
24+
public async clone(): Promise<GitRepo> {
25+
const repoDir = path.join(os.tmpdir(), `${this.repository}-${this.ref}`);
26+
this.deleteDirectory(repoDir);
27+
this.repo = await Clone.clone(
28+
`https://x-access-token:${this.installationToken}@github.com/${this.fullRepoName}.git`,
29+
repoDir,
30+
{
31+
checkoutBranch: this.branch,
32+
},
33+
);
34+
const commit = await this.repo.getCommit(this.ref);
35+
await this.repo.setHeadDetached(commit.id());
36+
return this;
37+
}
38+
39+
public getDirectory(): string {
40+
return this.repo.workdir();
41+
}
42+
43+
public getMythXConfigFile(): IGitMythXConfig {
44+
const rawData = this.getFile("./gitmythx.json");
45+
if (!rawData) { return null; }
46+
return JSON.parse(rawData);
47+
}
48+
49+
public getFile(filePath: string): string {
50+
filePath = path.join(this.repo.workdir(), filePath);
51+
if (!fs.existsSync(filePath)) {
52+
return null;
53+
}
54+
return fs.readFileSync(filePath, "utf-8");
55+
}
56+
57+
public getOwner(): string {
58+
return this.ownerId.toString(10);
59+
}
60+
61+
private deleteDirectory(dir: string): void {
62+
try {
63+
rimraf.sync(dir);
64+
} catch (ignored) {}
65+
}
66+
67+
}

src/Services/Github/GithubAppService.ts

+30-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import Octokit from "@octokit/rest";
2+
import {GitRepo} from "../GitRepo";
3+
import logger from "../Logger";
4+
import {Analysis} from "../MythX/Analysis";
25
import githubAppInstallationService from "./GithubAppInstallationService";
36
import {CheckRunConclusion, CheckRunStatus, GithubEvent} from "./types";
47

@@ -19,11 +22,34 @@ export default class GithubAppService {
1922
}
2023

2124
public async initiateCheckRun(payload: GithubEvent): Promise<boolean> {
25+
logger.info(
26+
`Seting check run status to "started" for
27+
checkrun ${payload.check_run.id} (${payload.repository.full_name})`,
28+
);
2229
await this.setCheckStartedStatus(payload);
23-
24-
// TODO: run analisys
25-
26-
await this.setCheckCompletedStatus(payload, CheckRunConclusion.SUCCESS);
30+
try {
31+
logger.info("Fetching installation token...");
32+
const installationToken = await githubAppInstallationService.getInstallationToken(
33+
payload.repository.owner.login,
34+
payload.repository.name,
35+
);
36+
const repo = new GitRepo(
37+
payload.repository.owner.id,
38+
payload.repository.full_name,
39+
payload.repository.name,
40+
payload.check_run.head_branch,
41+
payload.check_run.head_sha,
42+
installationToken,
43+
);
44+
logger.info(`Cloning ${payload.repository.full_name}#${payload.check_run.head_sha}`);
45+
await repo.clone();
46+
const analysis = new Analysis(repo);
47+
await analysis.run();
48+
await this.setCheckCompletedStatus(payload, CheckRunConclusion.SUCCESS);
49+
} catch (e) {
50+
logger.error(e.message);
51+
await this.setCheckCompletedStatus(payload, CheckRunConclusion.FAILURE);
52+
}
2753
return true;
2854
}
2955

src/Services/Github/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ interface CheckRun extends Check {
3838
}
3939

4040
interface Owner {
41+
id: number;
4142
login: string;
4243
}
4344

src/Services/MythX/Analysis.ts

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import * as armlet from "armlet";
2+
import * as path from "path";
3+
import User from "../../Models/User";
4+
import {IGitMythXConfig} from "../GitMythXConfig";
5+
import {GitRepo} from "../GitRepo";
6+
import logger from "../Logger";
7+
import {SolidityUtils} from "../SolidityUtils";
8+
import {AnalysisReport} from "./AnalysisReport";
9+
10+
export class Analysis {
11+
12+
private readonly config: IGitMythXConfig;
13+
14+
constructor(private readonly repo: GitRepo) {
15+
this.config = repo.getMythXConfigFile();
16+
}
17+
18+
public async run(): Promise<AnalysisReport[] | AnalysisReport> {
19+
if (!this.config) {
20+
logger.error("Missing gitmythx.json config in repo");
21+
return new AnalysisReport(
22+
false,
23+
"Missing gitmythx.json config in repo",
24+
);
25+
}
26+
return await Promise.all(
27+
this.config.contracts.map((contractConfig) => {
28+
return this.runAnalyses(contractConfig.name, this.repo.getFile(contractConfig.path));
29+
}),
30+
);
31+
32+
}
33+
34+
private async runAnalyses(contractFilePath: string, contractSource: string): Promise<AnalysisReport> {
35+
const contractFileName = path.basename(contractFilePath);
36+
const solcInput = {
37+
language: "Solidity",
38+
sources: {
39+
[contractFileName]: {
40+
content: contractSource,
41+
},
42+
},
43+
settings: {
44+
outputSelection: {
45+
"*": {
46+
"*": ["*"],
47+
"": ["ast"],
48+
},
49+
},
50+
optimizer: {
51+
enabled: true,
52+
runs: 200,
53+
},
54+
},
55+
};
56+
logger.info("Processing contract import paths");
57+
const importPaths = SolidityUtils.getImportPaths(contractSource);
58+
let sourceList = {};
59+
importPaths.forEach((importPath) => {
60+
const newSourceList = SolidityUtils.parseImports(path.dirname(contractFilePath), importPath, false);
61+
sourceList = {
62+
...sourceList,
63+
...newSourceList,
64+
};
65+
});
66+
solcInput.sources = {
67+
...solcInput.sources,
68+
...sourceList,
69+
};
70+
const mythxSourceList = Object.keys(sourceList);
71+
mythxSourceList.push(contractFileName);
72+
const compiled = await SolidityUtils.compile(solcInput, this.config.solidityVersion);
73+
if (!compiled.contracts || !Object.keys(compiled.contracts).length) {
74+
if (compiled.errors) {
75+
for (const compiledError of compiled.errors) {
76+
logger.error(compiledError.formattedMessage);
77+
}
78+
}
79+
return new AnalysisReport(
80+
false,
81+
"Failed to compšile contracts",
82+
);
83+
}
84+
85+
const inputfile = compiled.contracts[contractFileName];
86+
let contract;
87+
let contractName;
88+
89+
if (inputfile.length === 0) {
90+
logger.error("✖ No contracts found");
91+
return new AnalysisReport(
92+
false,
93+
"✖ No contracts found",
94+
);
95+
} else if (inputfile.length === 1) {
96+
contractName = Object.keys(inputfile)[0];
97+
contract = inputfile[contractName];
98+
} else {
99+
100+
/*
101+
* Get the contract with largest bytecode object to generate MythX analysis report.
102+
* If inheritance is used, the main contract is the largest as it contains the bytecode of all others.
103+
*/
104+
105+
const bytecodes = {};
106+
107+
for (const key in inputfile) {
108+
if (inputfile.hasOwnProperty(key)) {
109+
bytecodes[inputfile[key].evm.bytecode.object.length] = key;
110+
}
111+
}
112+
113+
const largestBytecodeKey = Object.keys(bytecodes).reverse()[0];
114+
contractName = bytecodes[largestBytecodeKey];
115+
contract = inputfile[contractName];
116+
}
117+
118+
/* Bytecode would be empty if contract is only an interface */
119+
120+
if (!contract.evm.bytecode.object) {
121+
logger.error(
122+
"✖ Compiling the Solidity code did not return any bytecode." +
123+
" Note that abstract contracts cannot be analyzed.",
124+
);
125+
return new AnalysisReport(
126+
false,
127+
"✖ Compiling the Solidity code did not return any bytecode." +
128+
" Note that abstract contracts cannot be analyzed.");
129+
}
130+
131+
/* Format data for MythX API */
132+
logger.info("Formatting mythx request...");
133+
const data = {
134+
contractName,
135+
bytecode: SolidityUtils.replaceLinkedLibs(contract.evm.bytecode.object),
136+
sourceMap: contract.evm.deployedBytecode.sourceMap,
137+
deployedBytecode: SolidityUtils.replaceLinkedLibs(contract.evm.deployedBytecode.object),
138+
deployedSourceMap: contract.evm.deployedBytecode.sourceMap,
139+
mythxSourceList,
140+
analysisMode: "quick",
141+
sources: {},
142+
mainSource: contractFileName,
143+
} as any;
144+
145+
// tslint:disable-next-line:forin
146+
for (const key in solcInput.sources) {
147+
data.sources[key] = { source: solcInput.sources[key].content };
148+
}
149+
logger.info(`Fetching mythx credentials for user ${this.repo.getOwner()}`);
150+
const mythxUser = await User.findOne({ where: { id: this.repo.getOwner() }});
151+
if (!mythxUser) {
152+
logger.error(`Missing mythx credentials for user ${this.repo.getOwner()}`);
153+
return new AnalysisReport(false, `Missing mythx credentials for user ${this.repo.getOwner()}`);
154+
}
155+
const client = new armlet.Client({
156+
ethAddress: "0x",
157+
password: "sample",
158+
});
159+
client.accessToken = mythxUser.accessToken;
160+
client.refreshToken = mythxUser.refreshToken;
161+
logger.info("Submitting contracts to mythx analysis");
162+
try {
163+
const result = await client.analyzeWithStatus({
164+
data,
165+
timeout: 300000,
166+
clientToolName: "GitMythX",
167+
});
168+
169+
/* Add `solidity_file_path` to display the result in the ESLint format with the provided input path */
170+
data.filePath = contractFilePath;
171+
172+
/* Add all the imported contracts source code to the `data` to sourcemap the issue location */
173+
data.sources = { ...solcInput.sources };
174+
logger.info("Creating analysis report...");
175+
const report = new AnalysisReport(true, "", data, result);
176+
logger.info("Created report!");
177+
logger.info(JSON.stringify(report));
178+
return report;
179+
} catch (e) {
180+
logger.error(e.message);
181+
}
182+
183+
}
184+
185+
}

0 commit comments

Comments
 (0)