Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1057fbd
Separate analysis from Markdown printing
cb-elileers Nov 8, 2024
bb768c1
Remove console log of markdown
cb-elileers Nov 8, 2024
e906b23
Remove broken detector
cb-elileers Nov 8, 2024
58837ca
Fix recursive exploration of directories ending in .sol
cb-elileers Nov 13, 2024
1e1daad
Sarif support
cb-elileers Nov 14, 2024
8d66b1f
Skip test, script, and lib folders in recursive Exploration
cb-elileers Nov 14, 2024
585539b
Merge pull request #1 from cb-elileers/reorg-analyze
cb-elileers Nov 18, 2024
9e37445
Partial Revert of #43
cb-elileers Nov 18, 2024
56f2a06
Commander SARIF support
cb-elileers Nov 18, 2024
1d9f2b2
parameterize listing files in report
cb-elileers Nov 19, 2024
2a2b2cc
Merge pull request #2 from cb-elileers/revert-dravee
cb-elileers Nov 19, 2024
b31be9f
remove fingerprint temporarily
cb-elileers Nov 19, 2024
af76dd5
Sarif Regex Deduplication
cb-elileers Jan 23, 2025
e9c8281
Merge pull request #4 from cb-elileers/sarif-deduplication
cb-elileers Jan 23, 2025
84ee38a
severity filtering
cb-elileers Jan 29, 2025
dd29a4b
Add support for skipping detectors
cb-elileers Jan 29, 2025
6806e5b
Add IDs to each detector based on filename
cb-elileers Jan 29, 2025
ba3a0db
Merge pull request #5 from cb-elileers/severity-filtering
cb-elileers Jan 29, 2025
7c96d6f
Fix bug in skipDetectors
cb-elileers Feb 12, 2025
3994c85
Merge pull request #6 from cb-elileers/severity-filtering
cb-elileers Feb 12, 2025
8e68d18
Add 15 detectors
mcp-coinbase Apr 22, 2025
334be00
Merge pull request #8 from mcp-coinbase/main
cb-elileers Apr 22, 2025
7c96391
Update repo link in SARIF
mcp-coinbase Apr 23, 2025
6c4e9b8
Update sarif.ts
mcp-coinbase Apr 23, 2025
60bded3
Merge pull request #9 from mcp-coinbase/patch-1
cb-elileers Apr 23, 2025
5ec43eb
Update recursiveExploration function to have a parameter for skipping…
cb-elileers Apr 23, 2025
9588068
Merge pull request #10 from cb-elileers/fix-recursive-exploration
cb-elileers Apr 23, 2025
3fb8c6d
Update usage instructions in readme
cb-elileers Apr 23, 2025
29b0843
Merge pull request #11 from cb-elileers/fixreadme
cb-elileers Apr 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ report.md
.vscode/tasks.json
scope.txt
repos
reports
reports
.DS_Store
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll": true
"source.fixAll": "explicit"
}
}
61 changes: 51 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,61 @@
- [Installation](#installation)
- [Contributing](#contributing)

## Usage
### Installing 4naly3er

```bash
yarn analyze <BASE_PATH> <SCOPE_FILE> <GITHUB_URL>
#### Prerequisites:

You must have `node` and `yarn` installed on your system.

#### Installation:

```
git clone [https://github.com/cb-elileers/analyzer](https://github.cbhq.net/security/solidity-analyzer)
cd analyzer
npm i --force --save-dev
yarn
```

## Using 4naly3er

# Example
yarn analyze contracts scope.example.txt
#### Basic Usage

```
yarn analyze <BASE_PATH> <OPTIONS>
```

- `BASE_PATH` is a relative path to the folder containing the smart contracts.
- `SCOPE_FILE` is an optional file containing a specific smart contracts scope (see [scope.example.txt](./scope.example.txt))
- `GITHUB_URL` is an optional url to generate links to github in the report
- For remappings, add `remappings.txt` to `BASE_PATH`.
- The output will be saved in a `report.md` file.
For example: `yarn analyze ~/Documents/op-enclave/`

**Where Options Are:**

- BASE_PATH is a **required** parameter which points to the folder containing the smart contract project.
- '-s, --scope scopeFile' .txt file containing the contest scope
- '-g, --github githubURL' github url to generate links to code
- '-o, --out reportPath' Path for Markdown report
- '-l, --listfiles' List analyzed files in Markdown Report
- '--legacyscope scopeFile' Path for legacy scope file
- '--sarif [outputPath]' Generate SARIF report, optionally include path to report. Default is analyzer.sarif
- '--skip-info' Skip info issues
- '--skip-gas' Skip gas issues
- '--skip-low' Skip low issues
- '--skip-medium' Skip medium issues
- '--skip-high' Skip high issues
- '--skip, --skip-detectors detectorID' Skip specific detectors by id

For any remappings, Forge can generate, or you can add, remappings.txt to the BASE_PATH and 4naly3er will use them accordingly.

Output from the tool is stored in **report.md** within the 4naly3er folder. To keep all documents related to a project together, it is advisable to run `mv report.md <BASE_PATH>` to deposit the report into the smart contract project's folder. (Click here to see an example report)[https://gist.github.com/Picodes/e9f1bb87ae832695694175abd8f9797f]

#### Scope File Generation

Sometimes, we only want to run our tooling on certain contracts within a repository. To do so, we define those contracts we want to be in scope in a **scope.txt** file.

To autogenerate a scope.txt file that excludes dependencies, we can use the below script:

```
cd <BASE_PATH>
find . | grep "\.sol" | grep -v "\./lib/" | grep -v typechain | grep -v node_modules | grep -v artifacts | grep -v "\.t\.sol">scope.txt
```

## Example Reports

Expand Down
69 changes: 4 additions & 65 deletions src/analyze.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InputType, Instance, Issue, IssueTypes } from './types';
import { InputType, Instance, Issue, Analysis } from './types';
import { lineFromIndex } from './utils';

const issueTypesTitles = {
Expand All @@ -13,9 +13,9 @@ const issueTypesTitles = {
* @notice Runs the given issues on files and generate the report markdown string
* @param githubLink optional url to generate links
*/
const analyze = (files: InputType, issues: Issue[], githubLink?: string): string => {
const analyze = (files: InputType, issues: Issue[], githubLink?: string): Analysis[] => {
let result = '';
let analyze: { issue: Issue; instances: Instance[] }[] = [];
let analyze: Analysis[] = [];
for (const issue of issues) {
let instances: Instance[] = [];
// If issue is a regex
Expand Down Expand Up @@ -61,68 +61,7 @@ const analyze = (files: InputType, issues: Issue[], githubLink?: string): string
}
}

/** Summary */
let c = 0;
if (analyze.length > 0) {
result += `\n## ${issueTypesTitles[analyze[0].issue.type]}\n\n`;
result += '\n| |Issue|Instances|\n|-|:-|:-:|\n';
for (const { issue, instances } of analyze) {
c++;
result += `| [${issue.type}-${c}](#${issue.type}-${c}) | ${issue.title} | ${instances.length} |\n`;
}
}

/** Issue breakdown */
c = 0;
for (const { issue, instances } of analyze) {
c++;
result += `### <a name="${issue.type}-${c}"></a>[${issue.type}-${c}] ${issue.title}\n`;
if (!!issue.description) {
result += `${issue.description}\n`;
}
if (!!issue.impact) {
result += '\n#### Impact:\n';
result += `${issue.impact}\n`;
}
result += `\n*Instances (${instances.length})*:\n`;
let previousFileName = '';
for (const o of instances.sort((a, b) => {
if (a.fileName < b.fileName) return -1;
if (a.fileName > b.fileName) return 1;
return !!a.line && !!b.line && a.line < b.line ? -1 : 1;
})) {
if (o.fileName !== previousFileName) {
if (previousFileName !== '') {
result += `\n${'```'}\n`;
if (!!githubLink) {
result += `[Link to code](${githubLink + previousFileName})\n`;
}
result += `\n`;
}
result += `${'```'}solidity\nFile: ${o.fileName}\n`;
previousFileName = o.fileName;
}

// Insert code snippet
const lineSplit = o.fileContent?.split('\n');
const offset = o.line.toString().length;
result += `\n${o.line}: ${lineSplit[o.line - 1]}\n`;
if (!!o.endLine) {
let currentLine = o.line + 1;
while (currentLine <= o.endLine) {
result += `${' '.repeat(offset)} ${lineSplit[currentLine - 1]}\n`;
currentLine++;
}
}
}
result += `\n${'```'}\n`;
if (!!githubLink) {
result += `[Link to code](${githubLink + previousFileName})\n`;
}
result += `\n`;
}

return result;
return analyze;
};

export default analyze;
51 changes: 44 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,49 @@ import main from './main';

// ================================= PARAMETERS ================================

const basePath =
process.argv.length > 2 ? (process.argv[2].endsWith('/') ? process.argv[2] : process.argv[2] + '/') : 'contracts/';
const scopeFile = process.argv.length > 3 && process.argv[3].endsWith('txt') ? process.argv[3] : null;
const githubLink = process.argv.length > 4 && process.argv[4] ? process.argv[4] : null;
const out = 'report.md';
import { program } from 'commander';
import { IssueTypes } from './types';

// ============================== GENERATE REPORT ==============================
program
.argument('[basePath]', 'Path were the contracts lies')
.option('-s, --scope <scopeFile>', '.txt file containing the contest scope')
.option('-g, --github <githubURL>', 'github url to generate links to code')
.option('-o, --out <reportPath>', 'Path for Markdown report')
.option('-l, --listfiles', 'List analyzed files in Markdown Report')
.option('--legacyscope <scopeFile>', 'Path for legacy scope file')
.option('--sarif [outputPath]', 'Generate SARIF report, optionally include path to report. Default is analyzer.sarif')
.option('--skip-info', 'Skip info issues')
.option('--skip-gas', 'Skip gas issues')
.option('--skip-low', 'Skip low issues')
.option('--skip-medium', 'Skip medium issues')
.option('--skip-high', 'Skip high issues')
.option('--skip, --skip-detectors <detectorID...>', 'Skip specific detectors by id')
.action((basePath:string, options) => {
basePath = basePath ||'contracts/';
basePath = basePath.endsWith('/') ? basePath : `${basePath}/`;

main(basePath, scopeFile, githubLink, out);
const sarif = options.sarif === true ? 'analyzer.sarif' : options.sarif;

const severityToRun: IssueTypes[] = [];
if(!options.skipInfo) severityToRun.push(IssueTypes.NC);
if(!options.skipGas) severityToRun.push(IssueTypes.GAS);
if(!options.skipLow) severityToRun.push(IssueTypes.L);
if(!options.skipMedium) severityToRun.push(IssueTypes.M);
if(!options.skipHigh) severityToRun.push(IssueTypes.H);

const skipDetectors = options.skipDetectors || [];
const skipDetectorsLower = skipDetectors.map(detector => detector.toLowerCase()); // Convert to lowercase to avoid case-sensitive issues

console.log(`basePath: ${basePath}`);
console.log(`scope: ${options.scope||'----'}`);
console.log(`github: ${options.github||'----'}`);
console.log(`out: ${options.out||'report.md'}`);
console.log(`legacyScope: ${options.legacyscope||'----'}`);
console.log(`sarif: ${options.sarif||'----'}`);
console.log('Severity to run: ', severityToRun);
console.log('Skipping detectors: ', skipDetectorsLower);

console.log('*****************************')
// ============================== RUN ANALYZER ==============================
main(basePath, options.scope, options.github, options.out || 'report.md', severityToRun, skipDetectorsLower, options.legacyscope, options.sarif, options.listfiles);
}).parse();
1 change: 1 addition & 0 deletions src/issues/GAS/ERC721A.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IssueTypes, RegexIssue } from '../../types';

const issue: RegexIssue = {
id: 'erc721A',
regexOrAST: 'Regex',
type: IssueTypes.GAS,
title: 'Use ERC721A instead ERC721',
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/_msgSender.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IssueTypes, RegexIssue } from '../../types';

const issue: RegexIssue = {
id: '_msgSender',
regexOrAST: 'Regex',
type: IssueTypes.GAS,
title: "Don't use `_msgSender()` if not supporting EIP-2771",
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/addPlusEqual.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IssueTypes, RegexIssue } from '../../types';

const issue: RegexIssue = {
id: 'addPlusEqual',
regexOrAST: 'Regex',
type: IssueTypes.GAS,
title: '`a = a + b` is more gas effective than `a += b` for state variables (excluding arrays and mappings)',
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/addressZero.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { instanceFromSRC } from '../../utils';
import { Expression, SourceUnit } from 'solidity-ast';

const issue: ASTIssue = {
id: 'addressZero',
regexOrAST: 'AST',
type: IssueTypes.GAS,
title: 'Use assembly to check for `address(0)`',
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/assignUpdateArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { findAll } from 'solidity-ast/utils';
import { instanceFromSRC } from '../../utils';

const issue: ASTIssue = {
id: 'assignUpdateArray',
regexOrAST: 'AST',
type: IssueTypes.GAS,
title: '`array[index] += amount` is cheaper than `array[index] = array[index] + amount` (or related variants)',
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/boolCompare.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IssueTypes, RegexIssue } from '../../types';

const issue: RegexIssue = {
id: 'boolCompare',
regexOrAST: 'Regex',
type: IssueTypes.GAS,
title: 'Comparing to a Boolean constant',
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/boolIncursOverhead.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IssueTypes, RegexIssue } from '../../types';

const issue: RegexIssue = {
id: 'boolIncursOverhead',
regexOrAST: 'Regex',
type: IssueTypes.GAS,
title: 'Using bools for storage incurs overhead',
Expand Down
11 changes: 11 additions & 0 deletions src/issues/GAS/bytesConstantsVsString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IssueTypes, RegexIssue } from '../../types';

const issue: RegexIssue = {
id: 'bytesConstantsVsString',
regexOrAST: 'Regex',
type: IssueTypes.GAS,
title: 'Bytes constants are more efficient than string constants',
regex: /string.+constant/g,
};

export default issue;
1 change: 1 addition & 0 deletions src/issues/GAS/cacheArrayLength.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IssueTypes, RegexIssue } from '../../types';

const issue: RegexIssue = {
id: 'cacheArrayLength',
regexOrAST: 'Regex',
type: IssueTypes.GAS,
title: 'Cache array length outside of loop',
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/cacheVariable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getStorageVariable, instanceFromSRC } from '../../utils';
import { Identifier } from 'solidity-ast';

const issue: ASTIssue = {
id: 'cacheVariable',
regexOrAST: 'AST',
type: IssueTypes.GAS,
title: 'State variables should be cached in stack variables rather than re-reading them from storage',
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/calldataViewFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ASTIssue, InputType, Instance, IssueTypes, RegexIssue } from '../../typ
import { instanceFromSRC } from '../../utils';

const issue: ASTIssue = {
id: 'calldataViewFunctions',
regexOrAST: 'AST',
type: IssueTypes.GAS,
title: 'Use calldata instead of memory for function arguments that do not get mutated',
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/canUseUnchecked.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IssueTypes, RegexIssue } from '../../types';

const issue: RegexIssue = {
id: 'canUseUnchecked',
regexOrAST: 'Regex',
type: IssueTypes.GAS,
title: 'For Operations that will not overflow, you could use unchecked',
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/customErrors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IssueTypes, RegexIssue } from '../../types';

const issue: RegexIssue = {
id: 'customErrors',
regexOrAST: 'Regex',
type: IssueTypes.GAS,
title: 'Use Custom Errors instead of Revert Strings to save Gas',
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/delegatecallAddressCheck.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IssueTypes, RegexIssue } from '../../types';

const issue: RegexIssue = {
id: 'delegatecallAddressCheck',
regexOrAST: 'Regex',
type: IssueTypes.GAS,
title: 'Avoid contract existence checks by using low level calls',
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/dontCacheIfUsedOnce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { instanceFromSRC } from '../../utils';
import util from 'util';

const issue: ASTIssue = {
id: 'dontCacheIfUsedOnce',
regexOrAST: 'AST',
type: IssueTypes.GAS,
title: 'Stack variable used as a cheaper cache for a state variable is only used once',
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/immutableConstructor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ASTIssue, InputType, Instance, IssueTypes, RegexIssue } from '../../typ
import { instanceFromSRC, topLevelFiles, getStorageVariable } from '../../utils';

const issue: ASTIssue = {
id: 'immutableConstructor',
regexOrAST: 'AST',
type: IssueTypes.GAS,
title: 'State variables only set in the constructor should be declared `immutable`',
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/initializeDefaultValue.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IssueTypes, RegexIssue } from '../../types';

const issue: RegexIssue = {
id: 'initializeDefaultValue',
regexOrAST: 'Regex',
type: IssueTypes.GAS,
title: "Don't initialize variables with default value",
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/longRevertString.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IssueTypes, RegexIssue } from '../../types';

const issue: RegexIssue = {
id: 'longRevertString',
regexOrAST: 'Regex',
type: IssueTypes.GAS,
title: 'Reduce the size of error messages (Long revert Strings)',
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/payableFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IssueTypes, RegexIssue } from '../../types';

const issue: RegexIssue = {
id: 'payableFunctions',
regexOrAST: 'Regex',
type: IssueTypes.GAS,
title: 'Functions guaranteed to revert when called by normal users can be marked `payable`',
Expand Down
1 change: 1 addition & 0 deletions src/issues/GAS/postIncrement.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IssueTypes, RegexIssue } from '../../types';

const issue: RegexIssue = {
id: 'postIncrement',
regexOrAST: 'Regex',
type: IssueTypes.GAS,
title: '`++i` costs less gas compared to `i++` or `i += 1` (same for `--i` vs `i--` or `i -= 1`)',
Expand Down
Loading