これは、なにをしたくて書いたもの?
以前、ast-grepを使ってJavaソースコードを検索してみました。
ASTベースのパターン検索ができるast-grepで、Javaのソースコードを検索する - CLOVER🍀
またSASTツールとしてSemgrepも使ってみました。
SASTツール、Semgrep Community Editionを試す - CLOVER🍀
SemgrepもルールはASTベースで書くので、ast-grepと同じようにASTベースでソースコードを検索できます。
ast-grepでやったことと同じことをSemgrepでもやってみようと思います。
Semgrepのパターン
Semgrepのパターン構文はこちらです。
いろいろ書いてあるのですが、チュートリアルをこなした方がわかりやすいかもですね。
またこちらで言語ごとのパターンの例を見ることもできます。
Rule pattern syntax examples | Semgrep
Semgrepでのメタ変数は$で表現します。$CLASSなどで任意の定義にマッチできます。0個以上のシーケンスは...で
マッチします。
このあたりは実際に使ってみた例を見た方が早いでしょうね。今回もJavaをお題にしてみます。
環境
今回の環境はこちら。
$ semgrep --version 1.155.0
Semgrepでソースコードを検索する
ast-grepの時と同じように、対象はRESTEasyのGitHubリポジトリーにします。
リポジトリーをclone。
$ git clone https://github.com/resteasy/resteasy
$ cd resteasy
検索は、semgrep scanで行います。scanは省略可です。
まずはクラス名で検索。
$ semgrep scan -l java -e 'ServerResponseWriter' $ semgrep -l java -e 'ServerResponseWriter' ## または $ semgrep scan --lang java --pattern 'ServerResponseWriter' $ semgrep --lang java --pattern 'ServerResponseWriter'
-eまたは--patternでパターンを指定して検索する場合、-lまたは--langオプションによる言語指定は必須です。
$ semgrep -e 'ServerResponseWriter' [00.06][ERROR]: -e/--pattern and -l/--lang must both be specified
結果はこちら。
$ semgrep -l java -e 'ServerResponseWriter' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:00 ┌─────────────────┐ │ 9 Code Findings │ └─────────────────┘ resteasy-core/src/main/java/org/jboss/resteasy/core/AsyncResponseConsumer.java 135┆ ServerResponseWriter.writeNomapResponse(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), ⋮┆---------------------------------------- 309┆ ServerResponseWriter.setResponseMediaType(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), ⋮┆---------------------------------------- 470┆ ServerResponseWriter.setResponseMediaType(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), resteasy-core/src/main/java/org/jboss/resteasy/core/SynchronousDispatcher.java 212┆ ServerResponseWriter.writeNomapResponse(((BuiltResponse) handledResponse), request, response, providerFactory, ⋮┆---------------------------------------- 474┆ ServerResponseWriter.writeNomapResponse((BuiltResponse) jaxrsResponse, request, response, providerFactory, ⋮┆---------------------------------------- 518┆ ServerResponseWriter.writeNomapResponse((BuiltResponse) jaxrsResponse, request, response, providerFactory, resteasy-core/src/main/java/org/jboss/resteasy/plugins/providers/sse/SseEventOutputImpl.java 144┆ ServerResponseWriter.writeNomapResponse(jaxrsResponse, request, response, ⋮┆---------------------------------------- 349┆ MediaType elementType = ServerResponseWriter.getResponseMediaType(jaxrsResponse, request, response, ⋮┆---------------------------------------- 425┆ ServerResponseWriter.writeNomapResponse(jaxrsResponse, request, response, ┌──────────────┐ │ Scan Summary │ └──────────────┘ ✅ Scan completed successfully. • Findings: 9 (9 blocking) • Rules run: 1 • Targets scanned: 914 • Parsed lines: ~100.0% • Scan skipped: ◦ Files matching .semgrepignore patterns: 3154 • Scan was limited to files tracked by git • For a detailed list of skipped files and lines, run semgrep with the --verbose flag Ran 1 rule on 914 files: 9 findings.
クラス自体の定義はヒットしないみたいですね…。クラス定義にマッチさせる場合は、クラス定義全体に対してになります。
最後のサマリーや進捗の表示が不要な場合は、-qまたは--quietオプションを指定します。
$ semgrep -q -l java -e 'ServerResponseWriter'
以後、この形式で書いていきます。
レシーバーを指定してのメソッド呼び出し。...を使っているので、引数は任意の数でマッチします。
$ semgrep -q -l java -e '$VAR.setResponseMediaType(...)' ┌─────────────────┐ │ 2 Code Findings │ └─────────────────┘ resteasy-core/src/main/java/org/jboss/resteasy/core/AsyncResponseConsumer.java 309┆ ServerResponseWriter.setResponseMediaType(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), 310┆ method); ⋮┆---------------------------------------- 470┆ ServerResponseWriter.setResponseMediaType(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), 471┆ method);
レシーバーの指定は任意にできなさそうです。
$ semgrep -q -l java -e 'setResponseMediaType(...)' ┌────────────────┐ │ 1 Code Finding │ └────────────────┘ resteasy-core/src/main/java/org/jboss/resteasy/core/ServerResponseWriter.java 98┆ setResponseMediaType(jaxrsResponse, request, response, providerFactory, method); $ semgrep -q -l java -e '....setResponseMediaType(...)' ┌─────────────────┐ │ 2 Code Findings │ └─────────────────┘ resteasy-core/src/main/java/org/jboss/resteasy/core/AsyncResponseConsumer.java 309┆ ServerResponseWriter.setResponseMediaType(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), 310┆ method); ⋮┆---------------------------------------- 470┆ ServerResponseWriter.setResponseMediaType(builtResponse, httpRequest, httpResponse, dispatcher.getProviderFactory(), 471┆ method);
メソッドの定義にマッチ。
$ semgrep -q -l java -e 'setResponseMediaType(...) { ... }' ┌────────────────┐ │ 1 Code Finding │ └────────────────┘ resteasy-core/src/main/java/org/jboss/resteasy/core/ServerResponseWriter.java 190┆ public static void setResponseMediaType(BuiltResponse jaxrsResponse, HttpRequest request, HttpResponse response, 191┆ ResteasyProviderFactory providerFactory, ResourceMethodInvoker method) { 192┆ MediaType mt = getResponseMediaType(jaxrsResponse, request, response, providerFactory, method); 193┆ if (mt != null) { 194┆ jaxrsResponse.getHeaders().putSingle(HttpHeaders.CONTENT_TYPE, mt); 195┆ } 196┆ }
logger変数でlogメソッドを呼び出している箇所にマッチ。
$ semgrep -q -l java -e 'logger.log(...)' ┌──────────────────┐ │ 10 Code Findings │ └──────────────────┘ resteasy-core/src/main/java/org/jboss/resteasy/core/SynchronousDispatcher.java 298┆ logger.log("MATCH_RESOURCE", invoker); ⋮┆---------------------------------------- 299┆ logger.log("MATCH_RESOURCE_METHOD", invoker.getMethod()); ⋮┆---------------------------------------- 386┆ logger.log("DISPATCH_RESPONSE", jaxrsResponse); resteasy-core/src/main/java/org/jboss/resteasy/core/registry/ClassNode.java 55┆ logger.log("MATCH_RUNTIME_RESOURCE", 56┆ expression, 57┆ expression.getRegex(), 58┆ expression.getRoot().root, 59┆ expression.getPathExpression()); resteasy-core/src/main/java/org/jboss/resteasy/core/registry/SegmentNode.java 75┆ logger.log("MATCH_PATH_FIND", path); ⋮┆---------------------------------------- 88┆ logger.log("MATCH_PATH_SKIPPED", expression.getRegex()); ⋮┆---------------------------------------- 128┆ logger.log("MATCH_LOCATOR", invoker.getMethod()); ⋮┆---------------------------------------- 137┆ logger.log("MATCH_PATH_NOT_MATCHED", expression.getRegex()); ⋮┆---------------------------------------- 148┆ logger.log("MATCH_PATH_SELECTED", match.match.expression.getRegex()); resteasy-core/src/main/java/org/jboss/resteasy/tracing/RESTEasyTracingLoggerImpl.java 137┆ logger.log(loggingLevel, 138┆ new StringBuilder() 139┆ .append(requestId) 140┆ .append(' ') 141┆ .append(event.name()) 142┆ .append(' ') 143┆ .append(message.toString()) 144┆ .append(" [") 145┆ .append(tracingInfo.formatDuration(duration)) 146┆ .append(" ms]") [hid 1 additional lines, adjust with --max-lines-per-finding]
指定の引数を使っているものにマッチ。
$ semgrep -q -l java -e 'logger.log("MATCH_RESOURCE", ...)' ┌────────────────┐ │ 1 Code Finding │ └────────────────┘ resteasy-core/src/main/java/org/jboss/resteasy/core/SynchronousDispatcher.java 298┆ logger.log("MATCH_RESOURCE", invoker);
メソッド呼び出し結果の代入にマッチ。
$ semgrep -q -l java -e '$VAR = $OBJECT.getResourceInvoker(...)' ┌─────────────────┐ │ 3 Code Findings │ └─────────────────┘ resteasy-core/src/main/java/org/jboss/resteasy/core/ResourceLocatorInvoker.java 162┆ ResourceInvoker invoker = registry.getResourceInvoker(request); resteasy-core/src/main/java/org/jboss/resteasy/core/SynchronousDispatcher.java 293┆ ResourceInvoker invoker = registry.getResourceInvoker(request); resteasy-core/src/main/java/org/jboss/resteasy/plugins/providers/AbstractPatchMethodFilter.java 160┆ resourceInovker = methodRegistry.getResourceInvoker(request);
ast-grepの結果と見比べると、テストコードが含まれていませんね…。
Semgrepも特定のクラスの中にあるメソッド定義にマッチさせることはできず、こう書くとすべてのserviceメソッドが
マッチします。なお、throwsを書かなくてもSemgrepの場合はマッチするみたいです…。
$ semgrep -q -l java -e 'service(...) { ... }' ┌─────────────────┐ │ 5 Code Findings │ └─────────────────┘ resteasy-core/src/main/java/org/jboss/resteasy/plugins/server/servlet/HttpServletDispatcher.java 48┆ @Override 49┆ protected void service(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) 50┆ throws ServletException, IOException { 51┆ service(httpServletRequest.getMethod(), httpServletRequest, httpServletResponse); 52┆ } ⋮┆---------------------------------------- 54┆ public void service(String httpMethod, HttpServletRequest request, HttpServletResponse response) throws IOException { 55┆ servletContainerDispatcher.service(httpMethod, request, response, true); 56┆ } resteasy-core/src/main/java/org/jboss/resteasy/plugins/server/servlet/ServletContainerDispatcher.java 188┆ public void service(String httpMethod, HttpServletRequest request, HttpServletResponse response, boolean handleNotFound) 189┆ throws IOException, NotFoundException { 190┆ try { 191┆ //logger.info(httpMethod + " " + request.getRequestURL().toString()); 192┆ //logger.info("***PATH: " + request.getRequestURL()); 193┆ // classloader/deployment aware RestasyProviderFactory. Used to have request specific 194┆ // ResteasyProviderFactory.getInstance() 195┆ ResteasyProviderFactory defaultInstance = ResteasyProviderFactory.getInstance(); 196┆ if (defaultInstance instanceof ThreadLocalResteasyProviderFactory) { 197┆ ThreadLocalResteasyProviderFactory.push(providerFactory); [hid 39 additional lines, adjust with --max-lines-per-finding] resteasy-jsapi/src/main/java/org/jboss/resteasy/jsapi/JSAPIServlet.java 55┆ @Override 56┆ protected void service(HttpServletRequest req, HttpServletResponse resp) 57┆ throws ServletException, IOException { 58┆ String pathInfo = req.getPathInfo(); 59┆ String uri = req.getRequestURL().toString(); 60┆ uri = uri.substring(0, uri.length() - req.getServletPath().length()); 61┆ if (LogMessages.LOGGER.isDebugEnabled()) { 62┆ LogMessages.LOGGER.debug(Messages.MESSAGES.serving(pathInfo)); 63┆ LogMessages.LOGGER.debug(Messages.MESSAGES.query(req.getQueryString())); 64┆ } [hid 14 additional lines, adjust with --max-lines-per-finding] resteasy-wadl/src/main/java/org/jboss/resteasy/wadl/ResteasyWadlServlet.java 51┆ @Override 52┆ protected void service(HttpServletRequest req, HttpServletResponse resp) 53┆ throws IOException { 54┆ String pathInfo = req.getPathInfo(); 55┆ String uri = req.getRequestURL().toString(); 56┆ uri = uri.substring(0, uri.length() - req.getServletPath().length()); 57┆ LogMessages.LOGGER.debug(Messages.MESSAGES.servingPathInfo(pathInfo)); 58┆ LogMessages.LOGGER.debug(Messages.MESSAGES.query(req.getQueryString())); 59┆ if (this.services == null) 60┆ scanResources(); [hid 7 additional lines, adjust with --max-lines-per-finding]
こんなところでしょうか。
おわりに
SemgrepでJavaソースコードをASTベースで検索してみました。
ast-grepとの比較のつもりで軽く試してみたのですが、けっこう性質が違いますね。
それに雰囲気で使うと見落としが多発しそうな気がするので、本格的に使う場合はそれなりにパターンを確認して使い方を
学んでからでしょうね。
これはSemgrep、ast-grepのどちらにも言えると思います。
速度は圧倒的にast-grepの方が速いのですが、あとはパターンの書き方、マッチ度合いの好みなどで決めるのでしょうか…。