From 2769b25bd41a18e9b5f9912b8c9ca1781c26b0ba Mon Sep 17 00:00:00 2001
From: smartcontracts <kelvin@optimism.io>
Date: Wed, 23 Oct 2024 10:23:17 +0700
Subject: [PATCH] feat(ci): add tests for semgrep rules (#12563)

Adds rule tests for all semgrep rules and fixes a few bugs that
were found during testing. Moves semgrep rules into the semgrep
folder without the . prefix because the prefix caused semgrep to
be unable to run the tests.
---
 .circleci/config.yml                 |   7 +-
 .semgrepignore                       |   2 +-
 justfile                             |   6 +-
 packages/contracts-bedrock/justfile  |   7 +-
 semgrep/sol-rules.t.sol              | 397 +++++++++++++++++++++++++++
 {.semgrep => semgrep}/sol-rules.yaml |  42 ++-
 6 files changed, 442 insertions(+), 19 deletions(-)
 create mode 100644 semgrep/sol-rules.t.sol
 rename {.semgrep => semgrep}/sol-rules.yaml (87%)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 1d4aec632..648217dbc 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -676,6 +676,8 @@ jobs:
       - run:
           name: print forge version
           command: forge --version
+      - run-contracts-check:
+          command: semgrep-test-validity-check
       - run-contracts-check:
           command: semgrep
       - run-contracts-check:
@@ -1298,7 +1300,10 @@ workflows:
       - semgrep-scan
       - semgrep-scan:
           name: semgrep-scan-local
-          scan_command: semgrep scan --timeout=100 --config=./.semgrep --error .
+          scan_command: semgrep scan --timeout=100 --config=./semgrep --error .
+      - semgrep-scan:
+          name: semgrep-test
+          scan_command: semgrep scan --test semgrep/
       - go-lint
       - fuzz-golang:
           name: fuzz-golang-<<matrix.package_name>>
diff --git a/.semgrepignore b/.semgrepignore
index 40e2e4ba1..0e7e4044b 100644
--- a/.semgrepignore
+++ b/.semgrepignore
@@ -9,7 +9,7 @@ vendor/
 *.min.js
 
 # Semgrep rules folder
-.semgrep
+semgrep/
 
 # Semgrep-action log folder
 .semgrep_logs/
diff --git a/justfile b/justfile
index 1ce2b2408..57d7fa5da 100644
--- a/justfile
+++ b/justfile
@@ -3,7 +3,11 @@ issues:
 
 # Runs semgrep on the entire monorepo.
 semgrep:
-  semgrep scan --config=.semgrep --error .
+  semgrep scan --config=semgrep --error .
+
+# Runs semgrep tests.
+semgrep-test:
+  semgrep scan --test semgrep/
 
 lint-shellcheck:
   find . -type f -name '*.sh' -not -path '*/node_modules/*' -not -path './packages/contracts-bedrock/lib/*' -not -path './packages/contracts-bedrock/kout*/*' -exec sh -c 'echo \"Checking $1\"; shellcheck \"$1\"' _ {} \\;
diff --git a/packages/contracts-bedrock/justfile b/packages/contracts-bedrock/justfile
index 4abeb52f4..897114cdc 100644
--- a/packages/contracts-bedrock/justfile
+++ b/packages/contracts-bedrock/justfile
@@ -163,6 +163,10 @@ semver-natspec-check-no-build:
 # Checks that semver natspec is equal to the actual semver version.
 semver-natspec-check: build semver-natspec-check-no-build
 
+# Checks that the semgrep tests are valid.
+semgrep-test-validity-check:
+  forge fmt ../../semgrep/sol-rules.t.sol --check
+
 # Checks that forge test names are correctly formatted.
 lint-forge-tests-check:
   go run ./scripts/checks/names
@@ -191,12 +195,13 @@ validate-spacers: build validate-spacers-no-build
 
 # Runs semgrep on the contracts.
 semgrep:
-  cd ../../ && semgrep scan --config=.semgrep ./packages/contracts-bedrock
+  cd ../../ && semgrep scan --config=semgrep ./packages/contracts-bedrock
 
 # TODO: Also run lint-forge-tests-check but we need to fix the test names first.
 # Runs all checks.
 check:
   @just gas-snapshot-check-no-build \
+  semgrep-test-validity-check \
   unused-imports-check-no-build \
   snapshots-check-no-build \
   lint-check \
diff --git a/semgrep/sol-rules.t.sol b/semgrep/sol-rules.t.sol
new file mode 100644
index 000000000..b697d94e9
--- /dev/null
+++ b/semgrep/sol-rules.t.sol
@@ -0,0 +1,397 @@
+// Semgrep tests for Solidity rules are defined in this file.
+// Semgrep tests do not need to be valid Solidity code but should be syntactically correct so that
+// Semgrep can parse them. You don't need to be able to *run* the code here but it should look like
+// the code that you expect to catch with the rule.
+//
+// Semgrep testing 101
+// Use comments like "ruleid: <rule-id>" to assert that the rule catches the code.
+// Use comments like "ok: <rule-id>" to assert that the rule does not catch the code.
+
+contract SemgrepTest__sol_safety_deployutils_args {
+    function test() {
+        // ruleid: sol-safety-deployutils-args
+        DeployUtils.create1AndSave({
+            _save: this,
+            _name: "SuperchainConfig",
+            _args: abi.encodeCall(ISuperchainConfig.__constructor__, ())
+        });
+
+        // ruleid: sol-safety-deployutils-args
+        DeployUtils.create1({ _name: "SuperchainConfig", _args: abi.encodeCall(ISuperchainConfig.__constructor__, ()) });
+
+        // ruleid: sol-safety-deployutils-args
+        DeployUtils.create2AndSave({
+            _save: this,
+            _salt: _implSalt(),
+            _name: "SuperchainConfig",
+            _args: abi.encodeCall(ISuperchainConfig.__constructor__, ())
+        });
+
+        // ruleid: sol-safety-deployutils-args
+        DeployUtils.create2({
+            _salt: _implSalt(),
+            _name: "SuperchainConfig",
+            _args: abi.encodeCall(ISuperchainConfig.__constructor__, ())
+        });
+
+        // ok: sol-safety-deployutils-args
+        DeployUtils.create1AndSave({
+            _save: this,
+            _name: "Proxy",
+            _nick: "DataAvailabilityChallengeProxy",
+            _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxy.__constructor__, (proxyAdmin)))
+        });
+
+        // ok: sol-safety-deployutils-args
+        DeployUtils.create1({
+            _name: "Proxy",
+            _nick: "DataAvailabilityChallengeProxy",
+            _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxy.__constructor__, (proxyAdmin)))
+        });
+
+        // ok: sol-safety-deployutils-args
+        DeployUtils.create2AndSave({
+            _save: this,
+            _salt: _implSalt(),
+            _name: "Proxy",
+            _nick: "DataAvailabilityChallengeProxy",
+            _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxy.__constructor__, (proxyAdmin)))
+        });
+
+        // ok: sol-safety-deployutils-args
+        DeployUtils.create2({
+            _salt: _implSalt(),
+            _name: "Proxy",
+            _nick: "DataAvailabilityChallengeProxy",
+            _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxy.__constructor__, (proxyAdmin)))
+        });
+    }
+}
+
+contract SemgrepTest__sol_safety_expectrevert_before_ll_call {
+    function test() {
+        // ok: sol-safety-expectrevert-before-ll-call
+        vm.expectRevert("some revert");
+        (bool revertsAsExpected,) = target.call(hex"");
+        assertTrue(revertsAsExpected);
+
+        // ok: sol-safety-expectrevert-before-ll-call
+        vm.expectRevert("some revert");
+        (bool revertsAsExpected,) = target.delegatecall(hex"");
+        assertTrue(revertsAsExpected);
+
+        // ok: sol-safety-expectrevert-before-ll-call
+        vm.expectRevert("some revert");
+        target.someFunction();
+
+        // ruleid: sol-safety-expectrevert-before-ll-call
+        vm.expectRevert("some revert");
+        (bool success,) = target.call(hex"");
+
+        // ruleid: sol-safety-expectrevert-before-ll-call
+        vm.expectRevert("some revert");
+        (bool success,) = target.call(hex"");
+        assertTrue(success);
+
+        // ruleid: sol-safety-expectrevert-before-ll-call
+        vm.expectRevert("some revert");
+        (bool success,) = target.delegatecall(hex"");
+        assertTrue(success);
+
+        // ruleid: sol-safety-expectrevert-before-ll-call
+        vm.expectRevert("some revert");
+        target.call(hex"");
+
+        // ruleid: sol-safety-expectrevert-before-ll-call
+        vm.expectRevert("some revert");
+        target.delegatecall(hex"");
+    }
+}
+
+contract SemgrepTest__sol_safety_expectrevert_no_args {
+    function test() {
+        // ok: sol-safety-expectrevert-no-args
+        vm.expectRevert("some revert");
+        target.someFunction();
+
+        // ruleid: sol-safety-expectrevert-no-args
+        vm.expectRevert();
+        target.someFunction();
+    }
+}
+
+contract SemgrepTest__sol_style_input_arg_fmt {
+    // ok: sol-style-input-arg-fmt
+    event Test(address indexed src, address indexed guy, uint256 wad);
+
+    // ok: sol-style-input-arg-fmt
+    function test() public {
+        // ...
+    }
+
+    // ok: sol-style-input-arg-fmt
+    function test(address payable) public {
+        // ...
+    }
+
+    // ok: sol-style-input-arg-fmt
+    function test(uint256 _a, uint256 _b) public {
+        // ...
+    }
+
+    // ok: sol-style-input-arg-fmt
+    function test(uint256 _a, uint256 _b) public {
+        // ...
+    }
+
+    // ok: sol-style-input-arg-fmt
+    function test(bytes memory _a) public {
+        // ...
+    }
+
+    // ok: sol-style-input-arg-fmt
+    function test(bytes memory _a, uint256 _b) public {
+        // ...
+    }
+
+    // ok: sol-style-input-arg-fmt
+    function test(Contract.Struct memory _a) public {
+        // ...
+    }
+
+    // ok: sol-style-input-arg-fmt
+    function test(uint256 _b, bytes memory) public {
+        // ...
+    }
+
+    // ok: sol-style-input-arg-fmt
+    function test(bytes memory, uint256 _b) public {
+        // ...
+    }
+
+    // ok: sol-style-input-arg-fmt
+    function test(bytes memory) public {
+        // ...
+    }
+
+    // ok: sol-style-input-arg-fmt
+    function test() public returns (bytes memory b_) {
+        // ...
+    }
+
+    // ruleid: sol-style-input-arg-fmt
+    function test(uint256 a) public {
+        // ...
+    }
+
+    // ruleid: sol-style-input-arg-fmt
+    function test(uint256 a, uint256 b) public {
+        // ...
+    }
+
+    // ruleid: sol-style-input-arg-fmt
+    function test(bytes memory a) public {
+        // ...
+    }
+
+    // ruleid: sol-style-input-arg-fmt
+    function testg(bytes memory a, uint256 b) public {
+        // ...
+    }
+
+    // ruleid: sol-style-input-arg-fmt
+    function test(uint256 b, bytes memory a) public {
+        // ...
+    }
+
+    // ruleid: sol-style-input-arg-fmt
+    function test(Contract.Struct memory a) public {
+        // ...
+    }
+
+    // ruleid: sol-style-input-arg-fmt
+    function test(uint256 _a, uint256 b) public {
+        // ...
+    }
+
+    // ruleid: sol-style-input-arg-fmt
+    function test(uint256 a, uint256 _b) public {
+        // ...
+    }
+}
+
+contract SemgrepTest__sol_style_return_arg_fmt {
+    // ok: sol-style-return-arg-fmt
+    function test() returns (uint256 a_) {
+        // ...
+    }
+
+    // ok: sol-style-return-arg-fmt
+    function test() returns (address payable) {
+        // ...
+    }
+
+    // ok: sol-style-return-arg-fmt
+    function test() returns (uint256 a_, bytes memory b_) {
+        // ...
+    }
+
+    // ok: sol-style-return-arg-fmt
+    function test() returns (Contract.Struct memory ab_) {
+        // ...
+    }
+
+    // ok: sol-style-return-arg-fmt
+    function test() returns (uint256, bool) {
+        // ...
+    }
+
+    // ok: sol-style-return-arg-fmt
+    function test() returns (uint256) {
+        // ...
+    }
+
+    // ruleid: sol-style-return-arg-fmt
+    function test() returns (uint256 a) {
+        // ...
+    }
+
+    // ruleid: sol-style-return-arg-fmt
+    function test() returns (uint256 a, bytes memory b) {
+        // ...
+    }
+
+    // ruleid: sol-style-return-arg-fmt
+    function test() returns (Contract.Struct memory b) {
+        // ...
+    }
+
+    // ruleid: sol-style-return-arg-fmt
+    function test() returns (Contract.Struct memory b, bool xyz) {
+        // ...
+    }
+}
+
+contract SemgrepTest__sol_style_doc_comment {
+    function test() {
+        // ok: sol-style-doc-comment
+        /// Good comment
+
+        // ok: sol-style-doc-comment
+        /// Multiline
+        /// Good
+        /// comment
+        /// @notice with natspec
+
+        // ruleid: sol-style-doc-comment
+        /**
+         * Example bad comment
+         */
+
+        // ruleid: sol-style-doc-comment
+        /**
+         * Example
+         * bad
+         * Multiline
+         * comment
+         */
+
+        // ruleid: sol-style-doc-comment
+        /**
+         * Example
+         * bad
+         * Multiline
+         * comment
+         * @notice with natspec
+         */
+    }
+}
+
+contract SemgrepTest__sol_style_malformed_require {
+    function test() {
+        // ok: sol-style-malformed-require
+        require(cond, "MyContract: test message good");
+
+        // ok: sol-style-malformed-require
+        require(cond, "MyContract: test message good");
+
+        // ok: sol-style-malformed-require
+        require(!LibString.eq(_standardVersionsToml, ""), "DeployImplementationsInput: not set");
+
+        // ok: sol-style-malformed-require
+        require(cond, "MyContract: Test message");
+
+        // ok: sol-style-malformed-require
+        require(cond, "L1SB-10");
+
+        // ok: sol-style-malformed-require
+        require(cond, "CHECK-L2OO-140");
+
+        // ok: sol-style-malformed-require
+        require(cond);
+
+        // ok: sol-style-malformed-require
+        require(bytes(env_).length > 0, "Config: must set DEPLOY_CONFIG_PATH to filesystem path of deploy config");
+
+        // ok: sol-style-malformed-require
+        require(false, string.concat("DeployConfig: cannot find deploy config file at ", _path));
+
+        // ok: sol-style-malformed-require
+        require(
+            _addrs[i] != _addrs[j],
+            string.concat(
+                "DeployUtils: check failed, duplicates at ", LibString.toString(i), ",", LibString.toString(j)
+            )
+        );
+
+        // ruleid: sol-style-malformed-require
+        require(cond, "MyContract: ");
+
+        // ruleid: sol-style-malformed-require
+        require(cond, "test");
+    }
+}
+
+contract SemgrepTest__sol_style_malformed_revert {
+    function test() {
+        // ok: sol-style-malformed-revert
+        revert("MyContract: test message good");
+
+        // ok: sol-style-malformed-revert
+        revert("MyContract: test message good");
+
+        // ok: sol-style-malformed-revert
+        revert("DeployImplementationsInput: not set");
+
+        // ok: sol-style-malformed-revert
+        revert("MyContract: Test message");
+
+        // ok: sol-style-malformed-revert
+        revert("L1SB-10");
+
+        // ok: sol-style-malformed-revert
+        revert("CHECK-L2OO-140");
+
+        // ok: sol-style-malformed-revert
+        revert();
+
+        // ok: sol-style-malformed-revert
+        revert("Config: must set DEPLOY_CONFIG_PATH to filesystem path of deploy config");
+
+        // ok: sol-style-malformed-revert
+        revert(string.concat("DeployConfig: cannot find deploy config file at ", _path));
+
+        // ok: sol-style-malformed-revert
+        revert(
+            string.concat(
+                "DeployUtils: check failed, duplicates at ", LibString.toString(i), ",", LibString.toString(j)
+            )
+        );
+
+        // ruleid: sol-style-malformed-revert
+        revert("MyContract: ");
+
+        // ruleid: sol-style-malformed-revert
+        revert("test");
+    }
+}
diff --git a/.semgrep/sol-rules.yaml b/semgrep/sol-rules.yaml
similarity index 87%
rename from .semgrep/sol-rules.yaml
rename to semgrep/sol-rules.yaml
index 8eaed9855..3c7422c25 100644
--- a/.semgrep/sol-rules.yaml
+++ b/semgrep/sol-rules.yaml
@@ -10,16 +10,34 @@ rules:
     severity: ERROR
     message: vm.expectRevert is followed by a low-level call but not followed by assertion expecting revert
     patterns:
-      - pattern: |
-          vm.expectRevert(...);
-          $CALL;
-          $CHECK;
+      - pattern-either:
+          - pattern: |
+              vm.expectRevert(...);
+              $CALL
+              $CHECK
+          - pattern: |
+              vm.expectRevert(...);
+              $CALL
       - metavariable-pattern:
           metavariable: $CALL
           patterns:
             - pattern-regex: \.call\(.*\)|\.delegatecall\(.*\)
-      - focus-metavariable: $CHECK
-      - pattern-not-regex: assertTrue\(revertsAsExpected\)
+      - pattern-not-inside:
+          patterns:
+            - pattern: |
+                vm.expectRevert(...);
+                $CALL;
+                assertTrue(revertsAsExpected);
+
+  - id: sol-safety-expectrevert-no-args
+    languages: [solidity]
+    severity: ERROR
+    message: vm.expectRevert() must specify the revert reason
+    patterns:
+      - pattern: vm.expectRevert()
+    paths:
+      exclude:
+        - packages/contracts-bedrock/test
 
   - id: sol-style-input-arg-fmt
     languages: [solidity]
@@ -62,15 +80,6 @@ rules:
       exclude:
         - packages/contracts-bedrock/test/safe-tools/CompatibilityFallbackHandler_1_3_0.sol
 
-  - id: sol-expectrevert-no-args
-    languages: [solidity]
-    severity: ERROR
-    message: vm.expectRevert() must specify the revert reason
-    patterns:
-      - pattern: vm.expectRevert()
-    paths:
-      exclude:
-        - packages/contracts-bedrock/test
 
   - id: sol-style-malformed-require
     languages: [solidity]
@@ -103,6 +112,9 @@ rules:
       - pattern-not: revert $ERR(...);
       - focus-metavariable: $MSG
       - pattern-not-regex: \"(\w+:\s[^"]+)\"
+      - pattern-not-regex: string\.concat\(\"(\w+:\s[^"]+)\"\,[^"]+\)
+      - pattern-not-regex: \"([a-zA-Z0-9\s]+-[a-zA-Z0-9\s]+)\"
+      - pattern-not-regex: \"([a-zA-Z0-9\s]+-[a-zA-Z0-9\s]+-[a-zA-Z0-9\s]+)\"
     paths:
       exclude:
         - packages/contracts-bedrock/test
-- 
2.23.0