├── .gitignore ├── .idea ├── .gitignore ├── modules.xml └── wally.iml ├── .wally.yaml ├── Dockerfile ├── LICENSE ├── README.md ├── Wally3.gif ├── assets ├── demo.mov ├── dual-node.svg ├── finding-node.svg ├── path-node.svg └── root-node.svg ├── checker └── checker.go ├── cmd ├── map.go ├── root.go ├── search.go └── server.go ├── go.mod ├── go.sum ├── graphsample.png ├── indicator └── indicator.go ├── logger └── logger.go ├── main.go ├── match └── match.go ├── navigator └── navigator.go ├── passes ├── callermapper │ └── callermapper.go ├── cefinder │ └── cefinder.go └── tokenfile │ └── tokenfile.go ├── reporter └── reporter.go ├── sampleapp ├── go.mod ├── main.go ├── printer │ └── printer.go └── safe │ └── safe.go ├── server ├── dist │ ├── browser.a9eca276.js │ ├── browser.a9eca276.js.map │ ├── index.585f762b.js │ ├── index.585f762b.js.map │ ├── index.e5f1a8e6.css │ ├── index.e5f1a8e6.css.map │ ├── index.html │ ├── index.runtime.ef08fcd2.js │ └── index.runtime.ef08fcd2.js.map ├── package-lock.json ├── package.json ├── server.go ├── src │ ├── cosmograph │ │ ├── config.ts │ │ ├── graph.ts │ │ └── types.ts │ ├── index.html │ ├── index.ts │ └── styles │ │ ├── main.css │ │ └── output.css ├── tailwind.config.js └── tsconfig.json ├── wallylib ├── callmapper │ └── callmapper.go ├── core.go ├── resolvers.go └── util.go └── wallynode ├── factory.go └── wallynode.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.[56789ao] 3 | *.a[56789o] 4 | *.so 5 | *.pyc 6 | ._* 7 | .nfs.* 8 | [56789a].out 9 | *~ 10 | *.orig 11 | *.rej 12 | *.exe 13 | .*.swp 14 | core 15 | *.cgo*.go 16 | *.cgo*.c 17 | _cgo_*/ 18 | 19 | go.work 20 | 21 | wally 22 | .idea/ 23 | 24 | .cache/ 25 | _dist/ 26 | node_modules/ 27 | .parcel-cache/ 28 | 29 | .prettierignore 30 | .prettierrc 31 | 32 | scripts/ 33 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/wally.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.wally.yaml: -------------------------------------------------------------------------------- 1 | indicators: 2 | - package: "github.com/hashicorp/nomad/command/agent" 3 | type: "" 4 | function: "forward" 5 | indicatorType: 1 6 | params: 7 | - name: "method" 8 | # - package: "github.com/hashicorp/nomad/nomad" 9 | # type: "" 10 | # function: "RPC" 11 | # indicatorType: 1 12 | # params: 13 | # - name: "method" 14 | # - package: "github.com/hashicorp/nomad/api" 15 | # type: "s" 16 | # function: "query" 17 | # indicatorType: 1 18 | # params: 19 | # - name: "endpoint" 20 | # pos: 0 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM golang:latest 3 | 4 | # Set the working directory inside the container 5 | WORKDIR /go/src/app 6 | 7 | # Copy the local package files to the container 8 | COPY . . 9 | RUN go mod download 10 | 11 | # Build the app 12 | RUN go build -o wally . 13 | 14 | ENTRYPOINT ["/go/src/app/wally"] 15 | 16 | # Copy the entire project to the container 17 | COPY *.go ./ 18 | 19 | # Build the app 20 | RUN go build -o wally . 21 | RUN go install . 22 | 23 | # Set the entry point to a shell 24 | ENTRYPOINT ["wally"] 25 | 26 | # Default command to run when the container starts 27 | CMD ["--help"] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Wally
3 | wally the cartographer 4 |

5 |
6 | 7 | Wally is a static analysis tool for mapping function paths in code. It can be used for: 8 | 9 | - HTTP and gRPC route detection 10 | - Attack surface mapping. 11 | - Automating the initial stages of threat modeling by mapping RPC and HTTP routes in Go code. 12 | - Planning fuzzing efforts by examining the fault tolerance of call paths in code. 13 | 14 | ## UI Demo 15 | 16 | https://github.com/hex0punk/wally/assets/1915998/1965f765-5437-4486-8c62-c125455b1f01 17 | 18 | _Read about this graph and how to explore it in the [Exploring the graph with wally server](#Exploring-the-graph-with-wally-server) section_ 19 | 20 | ## The basics 21 | 22 | ### Why is it called Wally? 23 | 24 | Because [Wally](https://monkeyisland.fandom.com/wiki/Wally_B._Feed) is a cartographer, I like Monkey Island, and I wanted it to be called that :). 25 | 26 | ### Why not just grep instead? 27 | 28 | So you are analyzing a Go-based application and you need to find all HTTP and RPC routes. You can run grep or ripgrep to find specific patterns that'd point you to routes in the code but: 29 | 30 | 1. You'd need to parse through a lot of unnecessary strings. 31 | 2. You may end up with functions that are similar to those you are targeting but have nothing to do with HTTP or RPC. 32 | 3. Grep won't solve constant values that indicate methods and route paths. 33 | 34 | ### What can Wally do that grep can't? 35 | 36 | Wally currently supports the following features: 37 | 38 | - Discover HTTP client calls and route listeners in your code by looking at each function name, signature, and package to make sure it finds the functions that you actually care about. 39 | - Wally solves the value of compile-time constant values that may be used in the functions of interest. Wally does a pretty good job at finding constants and global variables and resolving their values for you so you don't have to chase those manually in code. 40 | - Determine possible paths to a target function and examine the fault tolerance of such path. This is particularly useful when determining fuzzing targets or evaluating of panics discovered during fuzzing efforts. 41 | - Wally will report the enclosing function where the function of interest is called. 42 | - Wally will also give you all possible call paths to your functions of interest. This can be useful when analyzing monorepos where service A calls service B via a client function declared in service B's packages. This feature requires that the target code base is buildable. 43 | - Wally will output a nice PNG graph of the call stacks for the different routes it finds. 44 | - Determine which callpaths in code are tolerant to panics or application crashes due to bugs like nil dereferences 45 | 46 | ### Use cases - example 47 | 48 | #### Mapping routes for security analysis 49 | 50 | You are conducting an analysis of a monorepo containing multiple microservices. Often, these sorts of projects rely heavily on gRPC, which generates code for setting up gRPC routes via functions that call [`Invoke`](https://pkg.go.dev/google.golang.org/grpc#Invoke). Other services can then use these functions to call each other. 51 | 52 | One of the built-in indicators in `wally` will allow it to find functions that call `Invoke` for gRPC routes, so you can get a nice list of all gRPC method calls for all your microservices. Further, with `--ssa` you can also map the chains of methods gRPC calls necessary to reach any given gRPC route. With `wally`, you can then answer: 53 | 54 | - Can users reach service `Y` hosted internally via service `A` hosted externally? 55 | - Which service would I have to initialize a call to send user input to service `X`? 56 | - What functions are there between service `A` and service `Y` that might sanitize or modify the input set to service `A`? 57 | 58 | #### Planning fuzzing efforts by examining call path tolerance 59 | 60 | Say you are evaluating some microservices code in a monorepo and found several functions that seemed like good fuzzing targets due to their complexity and the type of data they handle. However, before fuzzing just for fuzzing’s sake, you want to answer the following: 61 | 62 | - What are the different ways in which this function can be reached? 63 | - For instance, can it only be reached via a gRPC or HTTP call that eventually lands at the target function, or is there some other process (say, a task run daily by a different process) that can call it with user input (e.g., pulled from a user database) via a different call path? 64 | - If I find a panic here, how much would it matter? That is, would this be recovered by a function in the call path with a recover() in a defer block? 65 | 66 | To learn how to answer the above questions, jump to the section on [using wally to detect fault tolerance of call paths](##Using-Wally-in-Fuzzing-Efforts-to-Determine-Fault-Tolerance-of-Call-Paths) 67 | 68 | ## Wally configurations 69 | 70 | Wally needs a bit of hand-holding. Though it can also do a pretty good job at guessing paths, it helps a lot if you tell it the packages and functions to look for, along with the parameters that you are hoping to discover and map. So, to help Wally do the job, you can specify a configuration file in YAML that defines a set of indicators. 71 | 72 | > [!TIP] 73 | > If you are just interested in use cases of a single function, you can run Wally on [single function search mode](###Analyzing-individual-paths) 74 | 75 | Wally runs a number of `indicators` which are basically clues as to whether a function in code may be related to a gRPC or HTTP route. By default, `wally` has a number of built-in `indicators` which check for common ways to set up and call HTTP and RPC methods using standard and popular libraries. However, sometimes a codebase may have custom methods for setting up HTTP routes or for calling HTTP and RPC services. For instance, when reviewing Nomad, you can give Wally the following configuration file with Nomad-specific indicators: 76 | 77 | ```yaml 78 | indicators: 79 | - id: nomad-1 80 | package: "github.com/hashicorp/nomad/command/agent" 81 | type: "" 82 | function: "forward" 83 | indicatorType: 1 84 | receiverType: "ACL" # optional 85 | params: 86 | - name: "method" 87 | - id: nomad-2 88 | package: "github.com/hashicorp/nomad/nomad" 89 | type: "" 90 | function: "RPC" 91 | indicatorType: 1 92 | params: # optional 93 | - name: "method" # optional 94 | - id: nomad-3 95 | package: "github.com/hashicorp/nomad/api" 96 | type: "s" 97 | function: "query" 98 | indicatorType: 1 99 | params: 100 | pos: 0 # optioncal 101 | ``` 102 | 103 | Note that you can specify the parameter that you want Wally to attempt to solve the value to. If you don't know the name of the parameter (per the function signature), you can give it the position in the signature. You can then use the `--config` or `-c` flag along with the path to the configuration file. 104 | 105 | ### Filtering Matches 106 | 107 | You can exclude the following from the analysis performed by Wally 108 | 109 | - **Packages**: Using `--exclude-pkg` you can enter a comma separated list of packages that wally will skip when collecting function call matches. 110 | - **Position**: Using `--exclude-pos` you can enter a comma separated list of code positions for function calls to skip when wally collects matches. Evaluation is suffix based, so you could pass strings like `my_file.go:X:Y` to avoid, for instance, analysis of matches that have a spurious number of paths. 111 | 112 | ## Match Filters vs. Path Filters 113 | 114 | You can provide a match filter or path filters to wally. 115 | 116 | - **Match Filters**. You can provide match filters via `--match-filter ` when using the `search` feature (see [single function search mode](###Analyzing-individual-paths)) in the CLI or `matchFilter` as a YAML element. Match filters are used by wally when collecting the initial set of matches and before performing path analysis (if using `--SSA` or the `search` feature). If a match filter is provided, Wally will check the function call site is in a package that contains the match filter as a prefix string. 117 | - **Path filters**. You can provide a path filter via the CLI via `-f `. The match filter is used during path analysis only. Wally makes sure that any functions that are part of a path to a match come from a package that contains the provided string a prefix. You can read more about this in the [Filtering call path analysis](#Filtering-call-path-analysis) section 118 | 119 | ## Route Detection 120 | 121 | A good test project to run it against is [nomad](https://github.com/hashicorp/nomad) because it has a lot of routes set up and called all over the place. I suggest the following: 122 | 123 | 1. Clone this project. 124 | 2. In a separate directory, clone [nomad](https://github.com/hashicorp/nomad). 125 | 3. Build this project by running `go build`. 126 | 4. Navigate to the root of the directory where you cloned nomad (`path/to/nomad`). 127 | 5. Create a configuration file named `.wally.yaml` with the content shown in the previous section of this README, and save it to the root of the nomad directory. 128 | 6. Run the following command from the nomad root: 129 | 130 | ```shell 131 | $ map -p ./... -vvv 132 | ``` 133 | 134 | ## Running Wally with Docker 135 | 136 | Wally can be easily run using Docker. Follow these steps: 137 | 138 | 1. Clone this project. 139 | 2. In a separate directory, clone [nomad](https://github.com/hashicorp/nomad). 140 | 3. Build the Docker Image: 141 | 142 | ```bash 143 | docker build -t go-wally . 144 | ``` 145 | 146 | 4. Run an interactive shell inside the Docker container 147 | 148 | ```bash 149 | docker run -it go-wally /bin/sh 150 | ``` 151 | 152 | 5. Run Wally with Docker, specifying the necessary parameters, such as the project path, configuration file, etc.: 153 | 154 | ```bash 155 | docker run -w // -v $(pwd):/ go-wally map //... -vvv 156 | ``` 157 | 158 | Adjust the flags (-p, -vvv, etc.) as needed for your use case. 159 | 160 | 6. If you have a specific configuration file (e.g., .wally.yaml), you can mount it into the container: 161 | 162 | ```bash 163 | docker run -w -v $(pwd): -v :/.wally.yaml go-wally map -c .wally.yaml -p ./... --max-paths 50 -vvv 164 | ``` 165 | 166 | This will run Wally within a Docker container, analyzing your Go code for HTTP and RPC routes based on the specified indicators and configurations. 167 | 168 | 7. Optionally, if you encountered any issues during the Docker build, you can revisit the interactive shell inside the container for further debugging. 169 | 170 | 8. After running Wally, you can check the results and the generated PNG or XDOT graph output, as explained in the README. 171 | 172 | 173 | ## Callpath analysis 174 | 175 | Wally should work even if you are not able to build the project you want to run it against. However, if you can build the project without any issues, you can run Wally using the `--ssa` flag, at which point Wally will be able to do the following: 176 | 177 | - Solve the enclosing function more effectively using [SSA](https://pkg.go.dev/golang.org/x/tools/go/ssa). 178 | - Output all possible call paths to the functions where the routes are defined and/or called. 179 | 180 | When using the `--ssa` flag you can expect output like this: 181 | 182 | ```shell 183 | ===========MATCH=============== 184 | ID: 14554c2a-41ee-4634-831d-6fc49c70c80d 185 | Indicator ID: 1 186 | Package: github.com/hashicorp/cronexpr 187 | Function: Parse 188 | Params: 189 | Enclosed by: (*github.com/hashicorp/nomad/nomad/structs.PeriodicConfig).Validate 190 | Position /Users/hex0punk/Tests/nomad/nomad/structs/structs.go:5638 191 | Possible Paths: 1 192 | Path 1 (filter limited): 193 | [Validate] nomad/structs/structs.go:5614:26 ---> 194 | 195 | ===========MATCH=============== 196 | ID: 6a876579-6b72-4501-af5b-5028c84a1c77 197 | Indicator ID: 1 198 | Package: github.com/hashicorp/cronexpr 199 | Function: Parse 200 | Params: 201 | Enclosed by: (*github.com/hashicorp/nomad/nomad/structs.PeriodicConfig).Validate 202 | Position /Users/hex0punk/Tests/nomad/nomad/structs/structs.go:5644 203 | Possible Paths: 1 204 | Path 1 (filter limited): 205 | [Validate] nomad/structs/structs.go:5614:26 ---> 206 | 207 | ===========MATCH=============== 208 | ID: eeaa94b1-28a8-41b8-a1e3-7a0d665a1e4d 209 | Indicator ID: 1 210 | Package: github.com/hashicorp/cronexpr 211 | Function: Parse 212 | Params: 213 | Enclosed by: github.com/hashicorp/nomad/nomad/structs.CronParseNext 214 | Position /Users/hex0punk/Tests/nomad/nomad/structs/structs.go:5677 215 | Possible Paths: 28 216 | Path 1 (RECOVERABLE): 217 | nomad.[Plan] nomad/job_endpoint.go:1949:57 ---> 218 | structs.[Next] nomad/structs/structs.go:5693:24 ---> 219 | [CronParseNext] (recoverable) nomad/structs/structs.go:5670:6 ---> 220 | Path 2 (RECOVERABLE): 221 | nomad.[Plan] nomad/job_endpoint.go:1949:57 ---> 222 | structs.[Next] nomad/structs/structs.go:5699:27 ---> 223 | [CronParseNext] (recoverable) nomad/structs/structs.go:5670:6 ---> 224 | Path 3 (node limited) (RECOVERABLE): 225 | nomad.[leaderLoop] nomad/leader.go:247:34 ---> 226 | nomad.[establishLeadership] nomad/leader.go:412:33 ---> 227 | nomad.[SetEnabled] nomad/periodic.go:167:3 ---> 228 | nomad.[run] nomad/periodic.go:332:14 ---> 229 | nomad.[dispatch] nomad/periodic.go:342:38 ---> 230 | structs.[Next] nomad/structs/structs.go:5693:24 ---> 231 | [CronParseNext] (recoverable) nomad/structs/structs.go:5670:6 ---> 232 | ``` 233 | 234 | ### Filtering call path analysis 235 | 236 | When running Wally in SSA mode against large codebases wally might run get lost in external libraries used by the target code. By default, Wally will filter call path functions to those belonging only to the module of each match discovered for a given indicator. This is what you'd want in most case. However, you can also filter analysis to only the packages container a string prefix which you can specify using `-f` followed by a string. For instance, when using wally to find HTTP and gRPC routes in nomad, you can to type the command below. 237 | 238 | ```shell 239 | $ wally map -p ./... --ssa -vvv -f "github.com/hashicorp/" --max-paths 50 240 | ``` 241 | 242 | Additionally, there may be cases where a module cannot be found for a given function match e.g., in the case the function belongs to the standard library). By setting a filter using `-f ` and keeping `module-only` set to `true`, wally will first default to filtering by the module strings and use the specified filter string whenever it cannot detect a module for a function match. 243 | 244 | Where `-f` defines a filter for the call stack search function. 245 | 246 | #### Using an empty filter with `--module-only=false` 247 | 248 | You can also allow wally to look beyond packages belonging to the target module by passing an empty filter and setting `module-only` to `false` (`-f "" --module-only=false`). However, keep in mind that doing so might result in wally getting stuck in some loop as it encounters recursive calls or very lengthy paths in scary dependency forests. 249 | 250 | > [!IMPORTANT] 251 | > You can also use `--max-paths` and an integer to limit the number of recursive calls Wally makes when mapping call paths (50 tends to be a good number). This will limit the paths you see in the output, but using a high enough number should still return helpful paths. Experiment with `--max-paths`, `--max-funcs`, `-f`, or all three to get the results you need or expect. 252 | 253 | Wally has the following options to limit the search. These options can help refine the results, but can be used for various experimental uses of Wally as well. 254 | 255 | - `--module-only`: Set to true by default. When set to true wally will restrict the call path search to the module of each function match. 256 | - `-f`: filter string which tells wally the path prefix for packages that you are interested in. Typically, you'd want to enter the full path for the Go module you are targetting, unless you are interested in paths that may reach to standard Go functions (i.e. `runtime`) via closures, etc. 257 | - `--max-paths`: maximum number of paths per match which wally will collect. This is helpful when the generate callgraphs report cyclic functions 258 | - `--max-funcs`: maxium number of functions or nodes reported per paths. We recommed you use this if you run wally without a filter using `-f` 259 | - `--skip-closures`: The default algorithm tries to be complete by returning possible ways in which closures can be used in the program, even if the path is not realistic given the program you are analyzing. This option reduces inaccurate paths by avoiding invalid paths from closures and instead skipping the enclosed function. 260 | - `--limiter-mode`: See explanation below 261 | 262 | #### Limiter modes 263 | 264 | At its core, Wally uses various algorithms available via the [golang.org/x/tools/go/callgraph](https://pkg.go.dev/golang.org/x/tools/go/callgraph) library. These algorithms can generate [spurious](https://pkg.go.dev/golang.org/x/tools/go/callgraph/cha) results at times which results in functions that go past main at the top of callpaths. To wrangle some of these sort of results, we perform a basic set of logical checks to eliminate or limit incorrect call path functions/nodes. By default, Wally uses the `very-strict` (level `4`) limiter mode. Use the other modes if you are not getting the results you expected. You can specify how the limiting is done using the `--limiter-mode` flag, followed by one of the modes levels below: 265 | 266 | - `4` (very-strict): **This is the default mode**. Wally will match node names to call site names (e.g., function `foo` can only be called by the name `foo` regardless of the place where it is called from). If the names don't match, wally will skip the node and keep searching. This will lead to the most accurate results, but in some cases such as when running Wally against very large codebases (and due to the large number of spurious nodes generated by the Go callgraph algorithms) this could take a while to finish. However, for most cases you likely won't need this mode 267 | - `3` (strict): Same as `skip-closures` plus all the restrictions below. In most cases, this mode and the `-f` filter is enough for getting the most accurate results. Typically, you won't _need_ to use `--max-paths` or `--max-funcs` when using this mode. 268 | - `2` (high): Wally will stop once it sees a function node `A` in the `main` _package_ followed by a call to B in any other package other than the `main` package where A was found. 269 | - `1` (normal): Wally will stop constructing call paths once it sees a call to either: 270 | - A function node A originating in the `main` _function_, followed by a call to node B not in the `main` function belonging to the same package 271 | - A function node A originating in the `main` _package_ followed by a call to node B inside the `main` function of a different package 272 | - A function node A originating in the `main` _pacckage_ followed by a function/node B not in the same package _unless_ function/node A is a closure. 273 | - `0` (none): Wally will construct call paths even past main if reported by the chosen `tools/go/callgraph` algorithm. 274 | 275 | #### Simple mode 276 | 277 | Using `-s` or `--simple` tells wally to only focus not on call sites but instead the relation between functions. In this mode, wally does the following: 278 | 279 | - It constructs paths not based on call sites but on containing functions. 280 | - It skips call sites 281 | - Skips closures, focusing instead on the enclosing functions 282 | - Removes duplicates, as this kinda of analysis would result in duplicated results otherwise 283 | 284 | This allows you to get a higher level view of the relation between packages, functions, etc. in your code. 285 | 286 | ### Analyzing individual paths 287 | 288 | Rather than using a yaml configuration file, you can use `wally map search` for mapping paths to individual functions. For instance: 289 | 290 | ```bash 291 | $ wally map search -p ./... --func Parse --pkg github.com/hashicorp/cronexpr -f github.com/hashicorp/ -vvv 292 | ``` 293 | The options above map to the following 294 | 295 | - `-p ./...`: Target code is in the current directory 296 | - `--func Parse`: We are interested only in the `Parse` function 297 | - `--pkg github.com/hashicorp/cronexpr`: Of package `github.com/hashicorp/cronexpr` 298 | - `-vvv`: Very, very verbose 299 | - `-f github.com/hashicorp/`: This tells Wally that we are only interested in paths within packages that start with `github.com/hashicorp/`. This avoids getting paths that reach beyond the scope we are interested in. Otherwise, we'd get nodes in standard Go libraries, etc. **Note:** this is optional, as by default wally will filter packages by the module string of each function match. 300 | 301 | ## Using Wally in Fuzzing Efforts to Determine Fault Tolerance of Call Paths 302 | 303 | Wally can now tell you which paths to a target function will recover in case of a panic triggered by that target function. A detailed explanation can be found [here](https://hex0punk.com/posts/fault-tolerance-detection-with-wally/). 304 | 305 | Using the [single function search mode](###Analyzing-individual-paths), we can determine which call paths to a given target function would recover in response to a panic 306 | 307 | ```shell 308 | $ wally map search -p ./... --func PrintOrPanic --pkg github.com/hex0punk/wally/sampleapp/printer -f github.com/hex0punk/wally/sampleapp -vvv 309 | 310 | ===========MATCH=============== 311 | ID: f9241d61-d19e-4847-b458-4f53a86ed5c5 312 | Indicator ID: 1 313 | Package: github.com/hex0punk/wally/samppleapp/printer 314 | Function: PrintOrPanic 315 | Params: 316 | Enclosed by: github.com/hex0punk/wally/samppleapp.printCharSafe$1 317 | Position /Users/alexuseche/Projects/wally/sampleapp/main.go:17 318 | Possible Paths: 1 319 | Path 1 (RECOVERABLE): 320 | main.[main] main.go:11:15 ---> 321 | main.[`printCharSafe`] main.go:16:16 ---> 322 | safe.[RunSafely] (recoverable) safe/safe.go:12:4 ---> 323 | main.[printCharSafe$1] main.go:16:17 ---> 324 | 325 | ===========MATCH=============== 326 | ID: eb72e837-31ba-4945-97b1-9432900ae3f9 327 | Indicator ID: 1 328 | Package: github.com/hex0punk/wally/samppleapp/printer 329 | Function: PrintOrPanic 330 | Params: 331 | Enclosed by: github.com/hex0punk/wally/samppleapp.printChar 332 | Position /Users/alexuseche/Projects/wally/sampleapp/main.go:22 333 | Possible Paths: 1 334 | Path 1: 335 | main.[main] main.go:12:11 ---> 336 | main.[printChar] main.go:21:6 ---> 337 | 338 | Total Results: 2 339 | ``` 340 | 341 | Paths marked with `(RECOVERABLE)` will be fault tolerant. The function containing the `recover()` block is marked in the results as `(recoverable)` 342 | 343 | ## Visualizing paths with wally 344 | 345 | To make visualization of callpaths easier, wally can lunch a server on localhost when via a couple methods: 346 | 347 | After an analysis by passing the `--server` flag to the `map` command. For instance: 348 | 349 | ```shell 350 | $ wally map -p ./... -c .wally.yaml --ssa -f "github.com/hashicorp/nomad" --server 351 | ``` 352 | 353 | Or, using the `server` subcommand and passing a wally json file: 354 | 355 | ```shell 356 | $ wally server -p ./nomad-wally.json -P 1984 357 | ``` 358 | 359 | Next, open a browser and head to the address in the output. 360 | 361 | ## Exploring the graph with wally server 362 | 363 | Graphs are generated using the [cosmograph](https://cosmograph.app/) library. Each node represents a function call in code. The colors are not random. Each color has a a different purpose to help you make good use of the graph. 364 | 365 | ![](assets/finding-node.svg) 366 | Finding node. This is a node discovered via wally indicators. Every finding node is the end of a path 367 | 368 | ![](assets/root-node.svg) 369 | This node is the root of a path to a finding node. 370 | 371 | ![](assets/path-node.svg) 372 | Intermediate node between a root and a finding node. 373 | 374 | ![](assets/dual-node.svg) 375 | This node servers both as the root node to a path and an intermediary node for one or more paths 376 | 377 | ### Viewing paths 378 | 379 | Clicking on any node will highlight all possible paths to that node. Click anywhere other than a node to exist the path selection view. 380 | 381 | ### Viewing findings 382 | 383 | Clicking on any finding node will populate the section on the left with information about the finding. 384 | 385 | ### Searching nodes 386 | 387 | Start typing on the search bar on the left to find a node by name. 388 | 389 | ### PNG and XDOT Graph output 390 | 391 | When using the `--ssa` flag, you can also use `-g` or `--graph` to indicate a path for a PNG or XDOT containing a Graphviz-based graph of the call stacks. For example, running: 392 | 393 | ```shell 394 | $ wally map -p ./... --ssa -vvv -f "github.com/hashicorp/nomad/" -g ./mygraph.png 395 | ``` 396 | 397 | From _nomad/command/agent_ will output this graph: 398 | 399 | ![](graphsample.png) 400 | 401 | Specifying a filename with a `.xdot` extension will create an [xdot](https://graphviz.org/docs/outputs/canon/#xdot) file instead. 402 | 403 | ## Advanced options 404 | 405 | - You can specify which algorithm to use for the intial callgraph generation using `--callgraph-alg`. This is the algorithm used by the `golang.org/x/tools/` function. Options include `cha` (default), [`rta`](https://pkg.go.dev/golang.org/x/tools/go/callgraph/rta), and [`vta`](https://pkg.go.dev/golang.org/x/tools/go/callgraph/vta). 406 | - By default, wally uses a breathd search first algorithm to map all paths. You can instead use depth first search using `--search-alg dfs` 407 | - Whenever Wally sees it reaches a `main` function, it will stop going further back in the tree to avoid reporting inaccurate paths. If you wish, you can override this by using the `--continue-after-main` flag, allowing you to see some interesting but less likely paths. 408 | 409 | ### Using DFS vs BFS 410 | 411 | When running Wally without `--max-paths` or `--max-funcs`, and with a `--limiter-mode` of `3` (Strict) or higher, both BFS (Breadth-First Search) and DFS (Depth-First Search) should identify the same nodes and sinks, ensuring that if a path from A to Z passing through D exists, both algorithms will find it. However, the handling of cyclic calls differs between the two approaches: 412 | 413 | - **Breadth-First Search (BFS)**: This method explores all nodes at the present depth level before moving on to nodes at the next depth level. BFS does not treat repeated calls to the same function within different branches as cycles unless the calls occur on the same line. This approach ensures that all potential paths are explored, making it ideal for comprehensive results, including paths with repeated function calls. Use BFS when you want to capture the most complete set of results, including paths where the same function may be called multiple times. 414 | 415 | - **Depth-First Search (DFS)**: This method explores as far as possible along a branch before backtracking. DFS treats repeated calls to the same function as cyclical if the call is encountered again within the same path, stopping the search at that point. This approach prevents revisiting the same functions multiple times within a single path, making it suitable for scenarios where only unique paths from start to finish are of interest. Use DFS when you are interested in finding all distinct paths from X to Z without regard to circular calls. 416 | 417 | This is intentional, as it allows the user to use either option to fit their needs. 418 | 419 | For example, consider the following path: 420 | 421 | ```shell 422 | - foo:128 423 | - bar:44 424 | - foo:124 425 | - myFunc:12 426 | ``` 427 | 428 | In this path, `foo` at line 128 calls `bar`, which in turn calls `foo` again at line 124, eventually calling `myFunc`. In this scenario, BFS will continue to collect nodes even if `foo` is called multiple times as long as `foo` is called from different positions (line and column number), while DFS will consider the second call to foo as part of a cycle and will not continue further along that path. 429 | 430 | - Use BFS: When you need the most comprehensive results, capturing all possible call paths, even those with repeated function calls. 431 | - Use DFS: When you want to avoid paths with repeated function calls, focusing instead on distinct paths from start to finish. 432 | 433 | Both methods will avoid cyclical calls if, say, `foo` is called twice in a path from the same line number. For instance, you will never see a result such as the one below: 434 | 435 | ```shell 436 | - foo:124 437 | - bar:44 438 | - foo:124 439 | - myFunc:12 440 | ``` 441 | 442 | ## The power of Wally 443 | 444 | At its core, Wally is, essentially, a function mapper. You can define functions in configuration files that have nothing to do with HTTP or RPC routes to obtain the same information that is described here. 445 | 446 | ## Logging 447 | 448 | You can add logging statements as needed during development in any function with a `Navigator` receiver like this: `n.Logger.Debug("your message", "a key", "a value")`. 449 | 450 | ## Troubleshooting 451 | 452 | At the moment, wally will often give you duplicate stack paths, where you'd notice a path of, say, A->B->C is repeated a couple of times or more. Based on my testing and debugging this is a drawback of the [`cha`](https://pkg.go.dev/golang.org/x/tools@v0.16.1/go/callgraph/cha) algorithm from Go's `callgraph` package, which wally uses for the call stack path functionality. I am experimenting with other available algorithms in `go/callgraph/` to determine what the best option to minimize such issues (while getting accurate call stacks) could be and will update wally's code accordingly. In the case that we stick to the `cha` algorithm, I will write code to filter duplicates. 453 | 454 | ### When running in SSA mode, I get findings with no enclosed functions reported 455 | 456 | This is often caused by issues in the target code base. Make sure you are able to build the target codebase. You may want to run `go build` and fix any issues reported by the compiler. Then, run wally again against it. 457 | 458 | ### Wally appears to be stuck in loop 459 | 460 | See the section on [Filtering call path analysis](#Filtering-call-path-analysis) 461 | 462 | ## Viewing help 463 | 464 | Viewing the description of each command 465 | 466 | ### `map` 467 | 468 | ```Shell 469 | $ wally map --help 470 | ``` 471 | 472 | #### `map search` (single function) 473 | 474 | ```Shell 475 | $ wally map search --help 476 | ``` 477 | 478 | ### `server` 479 | 480 | ```Shell 481 | $ wally map --help 482 | ``` 483 | 484 | ## Contributing 485 | 486 | Feel free to open issues and send PRs. Please. 487 | -------------------------------------------------------------------------------- /Wally3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hex0punk/wally/2c9b28d93a0d361333ac5622b61b02c1953a53fb/Wally3.gif -------------------------------------------------------------------------------- /assets/demo.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hex0punk/wally/2c9b28d93a0d361333ac5622b61b02c1953a53fb/assets/demo.mov -------------------------------------------------------------------------------- /assets/dual-node.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/finding-node.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/path-node.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/root-node.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /checker/checker.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "go/types" 5 | "golang.org/x/tools/go/analysis" 6 | "log" 7 | "reflect" 8 | ) 9 | 10 | type GlobalVar struct { 11 | Val string 12 | } 13 | 14 | func (*GlobalVar) AFact() {} 15 | 16 | func (*GlobalVar) String() string { return "GlobalVar" } 17 | 18 | type LocalVar struct { 19 | Vals []string 20 | } 21 | 22 | func (*LocalVar) AFact() {} 23 | 24 | func (*LocalVar) String() string { return "LocalVar" } 25 | 26 | type Checker struct { 27 | Analyzer *analysis.Analyzer 28 | //pkg *packages.Package 29 | //pass *analysis.Pass 30 | ObjectFacts map[objectFactKey]analysis.Fact 31 | } 32 | 33 | type objectFactKey struct { 34 | obj types.Object 35 | typ reflect.Type 36 | } 37 | 38 | func (c *Checker) ExportObjectFact(obj types.Object, fact analysis.Fact) { 39 | key := objectFactKey{ 40 | obj: obj, 41 | typ: factType(fact), 42 | } 43 | c.ObjectFacts[key] = fact 44 | } 45 | 46 | func (c *Checker) ImportObjectFact(obj types.Object, fact analysis.Fact) bool { 47 | if obj == nil { 48 | panic("nil object") 49 | } 50 | key := objectFactKey{obj, factType(fact)} 51 | if v, ok := c.ObjectFacts[key]; ok { 52 | reflect.ValueOf(fact).Elem().Set(reflect.ValueOf(v).Elem()) 53 | return true 54 | } 55 | return false 56 | } 57 | 58 | func InitChecker(analyzer *analysis.Analyzer) *Checker { 59 | return &Checker{ 60 | Analyzer: analyzer, 61 | ObjectFacts: map[objectFactKey]analysis.Fact{}, 62 | } 63 | } 64 | 65 | func factType(fact analysis.Fact) reflect.Type { 66 | t := reflect.TypeOf(fact) 67 | if t.Kind() != reflect.Ptr { 68 | log.Fatalf("invalid Fact type: got %T, want pointer", fact) 69 | } 70 | return t 71 | } 72 | -------------------------------------------------------------------------------- /cmd/map.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hex0punk/wally/indicator" 6 | "github.com/hex0punk/wally/navigator" 7 | "github.com/hex0punk/wally/reporter" 8 | "github.com/hex0punk/wally/server" 9 | "github.com/hex0punk/wally/wallylib/callmapper" 10 | "github.com/spf13/cobra" 11 | "gopkg.in/yaml.v2" 12 | "log" 13 | "os" 14 | "strings" 15 | ) 16 | 17 | var ( 18 | config string 19 | wallyConfig WallyConfig 20 | 21 | paths []string 22 | runSSA bool 23 | filter string 24 | graph string 25 | maxFuncs int 26 | maxPaths int 27 | printNodes bool 28 | format string 29 | outputFile string 30 | serverGraph bool 31 | skipDefault bool 32 | limiterMode int 33 | searchAlg string 34 | callgraphAlg string 35 | skipClosures bool 36 | moduleOnly bool 37 | simplify bool 38 | excludePkgs []string 39 | excluseByPosSuffix []string 40 | ) 41 | 42 | // mapCmd represents the map command 43 | var mapCmd = &cobra.Command{ 44 | Use: "map", 45 | Short: "Get list a list of all routes", 46 | Long: `Get list a list of all routes with resolved values as possible for params, along with enclosing functions"`, 47 | Args: func(cmd *cobra.Command, args []string) error { 48 | if format != "" && format != "json" { 49 | return fmt.Errorf("invalid output type: %q", format) 50 | } 51 | 52 | searchAlg = strings.ToLower(searchAlg) 53 | if searchAlg != "bfs" && searchAlg != "dfs" { 54 | return fmt.Errorf("search agorithm should be either bfs or dfs, got %s", searchAlg) 55 | } 56 | 57 | if callgraphAlg != "rta" && callgraphAlg != "cha" && callgraphAlg != "vta" && callgraphAlg != "static" { 58 | return fmt.Errorf("callgraph agorithm should be either cha, rta, or vta, got %s", callgraphAlg) 59 | } 60 | 61 | if limiterMode > 4 { 62 | return fmt.Errorf("limiter-mode should not be higher than 4, got %d", limiterMode) 63 | } 64 | 65 | if filter != "" && moduleOnly { 66 | fmt.Printf("You've set module-only to true with a non empty filter (%s). The module filter will only be used as a fallback in the case the that a module cannot be found during analysis. Set module-only to false if that is not the behavior you want\n", filter) 67 | } 68 | 69 | return nil 70 | }, 71 | Run: mapRoutes, 72 | } 73 | 74 | func init() { 75 | rootCmd.AddCommand(mapCmd) 76 | 77 | mapCmd.PersistentFlags().BoolVar(&skipDefault, "skip-default", false, "whether to skip the default indicators") 78 | mapCmd.PersistentFlags().IntVar(&limiterMode, "limiter-mode", 4, "Logic level to limit callgraph algorithm sporious nodes") 79 | mapCmd.PersistentFlags().StringVarP(&config, "config", "c", "", "path for config file containing indicators") 80 | mapCmd.PersistentFlags().StringVar(&callgraphAlg, "callgraph-alg", "cha", "cha || rta || vta") 81 | mapCmd.PersistentFlags().BoolVar(&skipClosures, "skip-closures", false, "Skip closure edges which can lead to innacurate results") 82 | mapCmd.PersistentFlags().BoolVar(&moduleOnly, "module-only", true, "Filter call paths by the match module.") 83 | mapCmd.PersistentFlags().BoolVarP(&simplify, "simple", "s", false, "Simple output focuses on function signatures rather than sites") 84 | 85 | mapCmd.PersistentFlags().StringSliceVarP(&paths, "paths", "p", paths, "The comma separated package paths to target. Use ./.. for current directory and subdirectories") 86 | mapCmd.PersistentFlags().StringVarP(&graph, "graph", "g", "", "Path for optional PNG graph output. Only works with --ssa") 87 | mapCmd.PersistentFlags().StringVar(&searchAlg, "search-alg", "bfs", "Search algorithm used for mapping callgraph (dfs or bfs)") 88 | mapCmd.PersistentFlags().BoolVar(&runSSA, "ssa", false, "whether to run some checks using SSA") 89 | mapCmd.PersistentFlags().StringVarP(&filter, "filter", "f", "", "Filter string for call graph search. Setting a non empty filter sets module-only to false") 90 | mapCmd.PersistentFlags().IntVar(&maxFuncs, "max-funcs", 0, "Limit the max number of nodes or functions per call path") 91 | mapCmd.PersistentFlags().IntVar(&maxPaths, "max-paths", 0, "Max paths per node. This helps when wally encounters recursive calls") 92 | mapCmd.PersistentFlags().BoolVar(&printNodes, "print-nodes", false, "Print the position of call graph paths rather than node") 93 | mapCmd.PersistentFlags().StringVar(&format, "format", "", "Output format. Supported: json, csv") 94 | mapCmd.PersistentFlags().StringVarP(&outputFile, "out", "o", "", "Output to file path") 95 | 96 | mapCmd.PersistentFlags().StringSliceVar(&excludePkgs, "exclude-pkg", []string{}, "Comma separated list of packages to exclude") 97 | mapCmd.PersistentFlags().StringSliceVar(&excluseByPosSuffix, "exclude-pos", []string{}, "Comma separated list of position prefixes used for filtering the selected function call matches") 98 | 99 | mapCmd.PersistentFlags().BoolVar(&serverGraph, "server", false, "Starts a server on port 1984 with output graph") 100 | } 101 | 102 | func mapRoutes(cmd *cobra.Command, args []string) { 103 | initConfig() 104 | 105 | indicators := indicator.InitIndicators(wallyConfig.Indicators, skipDefault) 106 | nav := navigator.NewNavigator(verbose, indicators) 107 | nav.RunSSA = runSSA 108 | nav.CallgraphAlg = callgraphAlg 109 | nav.Exclusions = navigator.Exclusions{ 110 | Packages: excludePkgs, 111 | PosSuffixes: excluseByPosSuffix, 112 | } 113 | 114 | nav.Logger.Info("Running mapper", "indicators", len(indicators)) 115 | 116 | nav.MapRoutes(paths) 117 | 118 | if len(nav.RouteMatches) == 0 { 119 | fmt.Println("No matches found") 120 | return 121 | } 122 | 123 | if runSSA { 124 | mapperOptions := callmapper.Options{ 125 | Filter: filter, 126 | MaxFuncs: maxFuncs, 127 | MaxPaths: maxPaths, 128 | PrintNodes: printNodes, 129 | SearchAlg: callmapper.SearchAlgs[searchAlg], 130 | Limiter: callmapper.LimiterMode(limiterMode), 131 | SkipClosures: skipClosures, 132 | ModuleOnly: moduleOnly, 133 | Simplify: simplify, 134 | } 135 | nav.Logger.Info("Solving call paths for matches", "matches", len(nav.RouteMatches)) 136 | nav.SolveCallPaths(mapperOptions) 137 | } 138 | nav.Logger.Info("Printing results") 139 | nav.PrintResults(format, outputFile) 140 | 141 | if runSSA && graph != "" { 142 | nav.Logger.Info("Generating graph", "graph filename", graph) 143 | reporter.GenerateGraph(nav.RouteMatches, graph) 144 | } 145 | 146 | if serverGraph { 147 | server.ServerCosmograph(reporter.GetJson(nav.RouteMatches), 1984) 148 | } 149 | } 150 | 151 | func initConfig() { 152 | wallyConfig = WallyConfig{} 153 | fmt.Println("Looking for config file in ", config) 154 | if _, err := os.Stat(config); os.IsNotExist(err) { 155 | fmt.Println("Configuration file `%s` not found. Will run stock indicators only", config) 156 | } else { 157 | data, err := os.ReadFile(config) 158 | if err != nil { 159 | log.Fatal(err) 160 | } 161 | 162 | err = yaml.Unmarshal([]byte(data), &wallyConfig) 163 | if err != nil { 164 | fmt.Println("Could not load configuration file: %s. Will run stock indicators only", err) 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/hex0punk/wally/indicator" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | type WallyConfig struct { 11 | Indicators []indicator.Indicator `yaml:"indicators"` 12 | } 13 | 14 | var ( 15 | verbose int 16 | ) 17 | 18 | // rootCmd represents the base command when called without any subcommands 19 | var rootCmd = &cobra.Command{ 20 | Use: "wally", 21 | Short: "Wally is a cartographer and helps you find and map HTTP and RPC routes in Go code", 22 | Long: `Wally is a cartographer from Scabb Island. 23 | He wears a monacle and claims to have traveled all over the world`, 24 | } 25 | 26 | // Execute adds all child commands to the root command and sets flags appropriately. 27 | // This is called by main.main(). It only needs to happen once to the rootCmd. 28 | func Execute() { 29 | err := rootCmd.Execute() 30 | if err != nil { 31 | os.Exit(1) 32 | } 33 | } 34 | 35 | func init() { 36 | rootCmd.PersistentFlags().CountVarP(&verbose, "verbose", "v", "verbose output. Up to -vvv levels of verbosity are supported") 37 | } 38 | -------------------------------------------------------------------------------- /cmd/search.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hex0punk/wally/indicator" 6 | "github.com/hex0punk/wally/navigator" 7 | "github.com/hex0punk/wally/reporter" 8 | "github.com/hex0punk/wally/server" 9 | "github.com/hex0punk/wally/wallylib/callmapper" 10 | "github.com/spf13/cobra" 11 | "strings" 12 | ) 13 | 14 | var ( 15 | pkg string 16 | function string 17 | recvType string 18 | matchFilters []string 19 | ) 20 | 21 | // funcCmd represents the map command 22 | var funcCmd = &cobra.Command{ 23 | Use: "search", 24 | Short: "Map a single function", 25 | Long: `Performs analysis given a single function"`, 26 | Run: searchFunc, 27 | Args: func(cmd *cobra.Command, args []string) error { 28 | if format != "" && format != "json" { 29 | return fmt.Errorf("invalid output type: %q", format) 30 | } 31 | 32 | searchAlg = strings.ToLower(searchAlg) 33 | if searchAlg != "bfs" && searchAlg != "dfs" { 34 | return fmt.Errorf("search agorithm should be either bfs or dfs, got %s", searchAlg) 35 | } 36 | 37 | if callgraphAlg != "rta" && callgraphAlg != "cha" && callgraphAlg != "vta" && callgraphAlg != "static" { 38 | return fmt.Errorf("callgraph agorithm should be either cha, rta, or vta, got %s", callgraphAlg) 39 | } 40 | 41 | if limiterMode > 4 { 42 | return fmt.Errorf("limiter-mode should not be higher than 4, got %d", limiterMode) 43 | } 44 | 45 | if filter != "" && moduleOnly { 46 | fmt.Printf("You've set module-only to true with a non empty filter (%s). The module filter will only be used as a fallback in the case the that a module cannot be found during analysis. Set module-only to false if that is not the behavior you want\n", filter) 47 | } 48 | 49 | return nil 50 | }, 51 | } 52 | 53 | func init() { 54 | mapCmd.AddCommand(funcCmd) 55 | funcCmd.PersistentFlags().StringVar(&pkg, "pkg", "", "Package name") 56 | funcCmd.PersistentFlags().StringVar(&function, "func", "", "Function name") 57 | funcCmd.PersistentFlags().StringVar(&recvType, "recv-type", "", "receiver type name (excluding package)") 58 | funcCmd.PersistentFlags().StringSliceVar(&matchFilters, "match-filter", []string{}, "Package prefix used for filtering the selected function call matches") 59 | funcCmd.MarkPersistentFlagRequired("pkg") 60 | funcCmd.MarkPersistentFlagRequired("func") 61 | } 62 | 63 | func searchFunc(cmd *cobra.Command, args []string) { 64 | indicators := indicator.InitIndicators( 65 | []indicator.Indicator{ 66 | { 67 | Package: pkg, 68 | Function: function, 69 | ReceiverType: recvType, 70 | MatchFilters: matchFilters, 71 | }, 72 | }, true, 73 | ) 74 | 75 | fmt.Println(len(matchFilters)) 76 | nav := navigator.NewNavigator(verbose, indicators) 77 | nav.RunSSA = true 78 | nav.CallgraphAlg = callgraphAlg 79 | 80 | mapperOptions := callmapper.Options{ 81 | Filter: filter, 82 | MaxFuncs: maxFuncs, 83 | MaxPaths: maxPaths, 84 | PrintNodes: printNodes, 85 | Limiter: callmapper.LimiterMode(limiterMode), 86 | SearchAlg: callmapper.SearchAlgs[searchAlg], 87 | SkipClosures: skipClosures, 88 | ModuleOnly: moduleOnly, 89 | Simplify: simplify, 90 | } 91 | 92 | nav.Logger.Info("Running mapper", "indicators", len(indicators)) 93 | nav.MapRoutes(paths) 94 | 95 | if len(nav.RouteMatches) == 0 { 96 | fmt.Printf("No matches found for func %s in package %s\n", function, pkg) 97 | return 98 | } 99 | 100 | nav.Logger.Info("Solving call paths for matches", "matches", len(nav.RouteMatches)) 101 | nav.SolveCallPaths(mapperOptions) 102 | 103 | nav.PrintResults(format, outputFile) 104 | 105 | if graph != "" { 106 | nav.Logger.Info("Generating graph", "graph filename", graph) 107 | reporter.GenerateGraph(nav.RouteMatches, graph) 108 | } 109 | 110 | if serverGraph { 111 | server.ServerCosmograph(reporter.GetJson(nav.RouteMatches), 1984) 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /cmd/server.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hex0punk/wally/server" 6 | "github.com/spf13/cobra" 7 | "log" 8 | "os" 9 | ) 10 | 11 | var ( 12 | jsonPath string 13 | port int 14 | ) 15 | 16 | var serverCmd = &cobra.Command{ 17 | Use: "server", 18 | Short: "Runs the wally server for exploring wally output", 19 | Long: `Runs the wally server for exploring wally output`, 20 | Run: serve, 21 | } 22 | 23 | func init() { 24 | rootCmd.AddCommand(serverCmd) 25 | serverCmd.PersistentFlags().StringVarP(&jsonPath, "json-path", "p", "", "Path for json file to visualize") 26 | serverCmd.PersistentFlags().IntVarP(&port, "port", "P", 1984, "Port number for wally server") 27 | } 28 | 29 | func serve(cmd *cobra.Command, args []string) { 30 | fmt.Println("Looking for wally file in ", jsonPath) 31 | if _, err := os.Stat(jsonPath); os.IsNotExist(err) { 32 | log.Fatalf("Wally file `%s` not found\n.", jsonPath) 33 | } 34 | 35 | data, err := os.ReadFile(jsonPath) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | server.ServerCosmograph(data, port) 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hex0punk/wally 2 | 3 | go 1.22.4 4 | 5 | require ( 6 | github.com/goccy/go-graphviz v0.1.2 7 | github.com/google/uuid v1.6.0 8 | github.com/spf13/cobra v1.8.0 9 | golang.org/x/tools v0.22.0 10 | gopkg.in/yaml.v2 v2.4.0 11 | ) 12 | 13 | require ( 14 | github.com/fogleman/gg v1.3.0 // indirect 15 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 16 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 17 | github.com/kr/pretty v0.3.1 // indirect 18 | github.com/pkg/errors v0.9.1 // indirect 19 | github.com/spf13/pflag v1.0.5 // indirect 20 | golang.org/x/image v0.14.0 // indirect 21 | golang.org/x/mod v0.18.0 // indirect 22 | golang.org/x/sync v0.7.0 // indirect 23 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/corona10/goimagehash v1.0.2 h1:pUfB0LnsJASMPGEZLj7tGY251vF+qLGqOgEP4rUs6kA= 2 | github.com/corona10/goimagehash v1.0.2/go.mod h1:/l9umBhvcHQXVtQO1V6Gp1yD20STawkhRnnX0D1bvVI= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 4 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 5 | github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= 6 | github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 7 | github.com/goccy/go-graphviz v0.1.2 h1:sWSJ6w13BCm/ZOUTHDVrdvbsxqN8yyzaFcHrH/hQ9Yg= 8 | github.com/goccy/go-graphviz v0.1.2/go.mod h1:pMYpbAqJT10V8dzV1JN/g/wUlG/0imKPzn3ZsrchGCI= 9 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 10 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 11 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 12 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 13 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 14 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 15 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 16 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 17 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 18 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 19 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 20 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 21 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 22 | github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 h1:BvoENQQU+fZ9uukda/RzCAL/191HHwJA5b13R6diVlY= 23 | github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 24 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 25 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 26 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 27 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 28 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 29 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 30 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 31 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 32 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 33 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 34 | golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= 35 | golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= 36 | golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= 37 | golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 38 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 39 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 40 | golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= 41 | golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= 42 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 43 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 44 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 45 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 46 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 47 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 48 | -------------------------------------------------------------------------------- /graphsample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hex0punk/wally/2c9b28d93a0d361333ac5622b61b02c1953a53fb/graphsample.png -------------------------------------------------------------------------------- /indicator/indicator.go: -------------------------------------------------------------------------------- 1 | package indicator 2 | 3 | import "fmt" 4 | 5 | type IndicatorType int 6 | 7 | const ( 8 | Service IndicatorType = iota 9 | Caller 10 | ) 11 | 12 | type ParamType int 13 | 14 | const ( 15 | HTTPMethod ParamType = iota 16 | Path 17 | ) 18 | 19 | type Indicator struct { 20 | Id string `yaml:"id"` 21 | Package string `yaml:"package"` 22 | Type string `yaml:"type"` 23 | Function string `yaml:"function"` 24 | Params []RouteParam `yaml:"params"` 25 | IndicatorType IndicatorType `yaml:"indicatorType"` 26 | ReceiverType string `yaml:"receiverType"` 27 | MatchFilters []string `yaml:"matchFilter"` 28 | } 29 | 30 | type RouteParam struct { 31 | Name string `yaml:"name"` 32 | Pos int `yaml:"pos"` 33 | } 34 | 35 | func InitIndicators(customIndicators []Indicator, skipDefault bool) []Indicator { 36 | indicators := []Indicator{} 37 | if !skipDefault { 38 | indicators = getStockIndicators() 39 | } 40 | 41 | if len(customIndicators) > 0 { 42 | fmt.Println("Loading custom indicator") 43 | idStart := len(indicators) 44 | for i, ind := range customIndicators { 45 | indCpy := ind 46 | if indCpy.Id == "" { 47 | indCpy.Id = fmt.Sprintf("%d", idStart+i+1) 48 | } 49 | fmt.Println("Pkg: ", indCpy.Package) 50 | fmt.Println("Func: ", indCpy.Function) 51 | if indCpy.ReceiverType != "" { 52 | fmt.Println("Receiver Type: ", indCpy.ReceiverType) 53 | } 54 | fmt.Println() 55 | indicators = append(indicators, indCpy) 56 | } 57 | } 58 | return indicators 59 | } 60 | 61 | func getStockIndicators() []Indicator { 62 | return []Indicator{ 63 | { 64 | Id: "1", 65 | Package: "net/http", 66 | Type: "", 67 | Function: "Handle", 68 | Params: []RouteParam{ 69 | {Name: "pattern"}, 70 | }, 71 | IndicatorType: Service, 72 | MatchFilters: []string{}, 73 | }, 74 | { 75 | Id: "2", 76 | Package: "google.golang.org/grpc", 77 | Type: "", 78 | Function: "Invoke", 79 | Params: []RouteParam{ 80 | {Name: "method"}, 81 | }, 82 | IndicatorType: Service, 83 | MatchFilters: []string{}, 84 | }, 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | ) 7 | 8 | func NewLogger(level int) *slog.Logger { 9 | verbosity := parseVerbosity(level) 10 | opts := &slog.HandlerOptions{Level: verbosity} 11 | 12 | logger := slog.New(slog.NewTextHandler(os.Stdout, opts)) 13 | return logger 14 | } 15 | 16 | func parseVerbosity(verbosityFlag int) slog.Level { 17 | switch verbosityFlag { 18 | case 2: 19 | return slog.LevelInfo 20 | case 3: 21 | return slog.LevelDebug 22 | default: 23 | return slog.LevelError 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2023 NAME HERE 3 | */ 4 | package main 5 | 6 | import "github.com/hex0punk/wally/cmd" 7 | 8 | func main() { 9 | cmd.Execute() 10 | } 11 | -------------------------------------------------------------------------------- /match/match.go: -------------------------------------------------------------------------------- 1 | package match 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/google/uuid" 7 | "github.com/hex0punk/wally/indicator" 8 | "github.com/hex0punk/wally/wallynode" 9 | "go/token" 10 | "go/types" 11 | "golang.org/x/tools/go/ssa" 12 | ) 13 | 14 | type RouteMatch struct { 15 | MatchId string 16 | Indicator indicator.Indicator // It should be FuncInfo instead 17 | Params map[string]string 18 | Pos token.Position 19 | Signature *types.Signature 20 | EnclosedBy string 21 | Module string 22 | SSA *SSAContext 23 | } 24 | 25 | // TODO: I don't love this here, maybe an SSA dedicated pkg would be better 26 | type SSAContext struct { 27 | PathLimited bool 28 | EnclosedByFunc *ssa.Function 29 | CallPaths *CallPaths 30 | SSAInstruction ssa.CallInstruction 31 | SSAFunc *ssa.Function 32 | TargetPos string 33 | } 34 | 35 | type CallPaths struct { 36 | Paths []*CallPath 37 | } 38 | 39 | type CallPath struct { 40 | ID int 41 | Nodes []wallynode.WallyNode 42 | NodeLimited bool 43 | FilterLimited bool 44 | Recoverable bool 45 | } 46 | 47 | func (cp *CallPaths) InsertPaths(nodes []wallynode.WallyNode, nodeLimited bool, filterLimited bool, simplify bool) { 48 | callPath := CallPath{NodeLimited: nodeLimited, FilterLimited: filterLimited} 49 | 50 | for _, node := range nodes { 51 | if simplify && node.Site != nil { 52 | continue 53 | } 54 | callPath.Nodes = append(callPath.Nodes, node) 55 | // Temp hack while we replace nodes with a structure containing parts of a path (func, pkg, etc.) 56 | if node.IsRecoverable() { 57 | callPath.Recoverable = true 58 | } 59 | } 60 | 61 | // Simplified output can result in duplicates, 62 | // as there can be multiple call sites inside the same enclosing function 63 | if simplify { 64 | for _, existingPath := range cp.Paths { 65 | if isSamePath(existingPath, callPath.Nodes) { 66 | return 67 | } 68 | } 69 | } 70 | 71 | cp.Paths = append(cp.Paths, &callPath) 72 | } 73 | 74 | func isSamePath(callPath *CallPath, nodes []wallynode.WallyNode) bool { 75 | if len(callPath.Nodes) != len(nodes) { 76 | return false 77 | } 78 | 79 | for i, existingPath := range callPath.Nodes { 80 | if existingPath.NodeString != nodes[i].NodeString { 81 | return false 82 | } 83 | } 84 | 85 | return true 86 | } 87 | 88 | func (cp *CallPaths) Print() { 89 | for _, callPath := range cp.Paths { 90 | fmt.Println("NODE: ", callPath) 91 | for i, p := range callPath.Nodes { 92 | fmt.Printf("%d Path: %s\n", i, p.NodeString) 93 | } 94 | } 95 | } 96 | 97 | func NewRouteMatch(indicator indicator.Indicator, pos token.Position) RouteMatch { 98 | return RouteMatch{ 99 | MatchId: uuid.New().String(), 100 | Indicator: indicator, 101 | Pos: pos, 102 | SSA: &SSAContext{}, 103 | } 104 | } 105 | func (r *RouteMatch) MarshalJSON() ([]byte, error) { 106 | var enclosedBy string 107 | if r.SSA != nil && r.SSA.EnclosedByFunc != nil { 108 | enclosedBy = r.SSA.EnclosedByFunc.String() 109 | } else { 110 | enclosedBy = r.EnclosedBy 111 | } 112 | 113 | params := make(map[string]string) 114 | for k, v := range r.Params { 115 | if v == "" { 116 | v = "" 117 | } 118 | if k == "" { 119 | k = "" 120 | } 121 | params[k] = v 122 | } 123 | 124 | var resPaths [][]string 125 | for _, paths := range r.SSA.CallPaths.Paths { 126 | var p []string 127 | for x := len(paths.Nodes) - 1; x >= 0; x-- { 128 | p = append(p, paths.Nodes[x].NodeString) 129 | } 130 | p = append(p, r.SSA.TargetPos) 131 | resPaths = append(resPaths, p) 132 | } 133 | 134 | return json.Marshal(struct { 135 | MatchId string 136 | Indicator indicator.Indicator 137 | Params map[string]string 138 | Pos string 139 | EnclosedBy string 140 | PathLimited bool 141 | Paths [][]string 142 | }{ 143 | MatchId: r.MatchId, 144 | Indicator: r.Indicator, 145 | Params: params, 146 | Pos: r.Pos.String(), 147 | EnclosedBy: enclosedBy, 148 | PathLimited: r.SSA.PathLimited, 149 | Paths: resPaths, 150 | }) 151 | } 152 | -------------------------------------------------------------------------------- /navigator/navigator.go: -------------------------------------------------------------------------------- 1 | package navigator 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hex0punk/wally/checker" 6 | "github.com/hex0punk/wally/indicator" 7 | "github.com/hex0punk/wally/logger" 8 | "github.com/hex0punk/wally/match" 9 | "github.com/hex0punk/wally/passes/callermapper" 10 | "github.com/hex0punk/wally/passes/cefinder" 11 | "github.com/hex0punk/wally/passes/tokenfile" 12 | "github.com/hex0punk/wally/reporter" 13 | "github.com/hex0punk/wally/wallylib" 14 | "github.com/hex0punk/wally/wallylib/callmapper" 15 | "go/ast" 16 | "go/token" 17 | "go/types" 18 | "golang.org/x/tools/go/analysis" 19 | "golang.org/x/tools/go/analysis/passes/ctrlflow" 20 | "golang.org/x/tools/go/analysis/passes/inspect" 21 | "golang.org/x/tools/go/ast/astutil" 22 | "golang.org/x/tools/go/ast/inspector" 23 | "golang.org/x/tools/go/callgraph" 24 | "golang.org/x/tools/go/callgraph/cha" 25 | "golang.org/x/tools/go/callgraph/rta" 26 | "golang.org/x/tools/go/callgraph/static" 27 | "golang.org/x/tools/go/callgraph/vta" 28 | "golang.org/x/tools/go/packages" 29 | "golang.org/x/tools/go/ssa" 30 | "golang.org/x/tools/go/ssa/ssautil" 31 | "log" 32 | "log/slog" 33 | "os" 34 | "strings" 35 | "sync" 36 | "time" 37 | ) 38 | 39 | type Navigator struct { 40 | Logger *slog.Logger 41 | SSA *SSA 42 | RouteIndicators []indicator.Indicator 43 | RouteMatches []match.RouteMatch 44 | RunSSA bool 45 | Packages []*packages.Package 46 | CallgraphAlg string 47 | Exclusions Exclusions 48 | } 49 | 50 | type Exclusions struct { 51 | Packages []string 52 | PosSuffixes []string 53 | } 54 | 55 | type SSA struct { 56 | Packages []*ssa.Package 57 | Callgraph *callgraph.Graph 58 | Program *ssa.Program 59 | } 60 | 61 | func NewNavigator(logLevel int, indicators []indicator.Indicator) *Navigator { 62 | return &Navigator{ 63 | Logger: logger.NewLogger(logLevel), 64 | RouteIndicators: indicators, 65 | } 66 | } 67 | 68 | // Copied from https://github.com/golang/tools/blob/master/cmd/callgraph/main.go#L291C1-L302C2 69 | func mainPackages(pkgs []*ssa.Package) ([]*ssa.Package, error) { 70 | var mains []*ssa.Package 71 | for _, p := range pkgs { 72 | if p != nil && p.Pkg.Name() == "main" && p.Func("main") != nil { 73 | mains = append(mains, p) 74 | } 75 | } 76 | if len(mains) == 0 { 77 | return nil, fmt.Errorf("no main packages") 78 | } 79 | return mains, nil 80 | } 81 | 82 | func (n *Navigator) MapRoutes(paths []string) { 83 | if len(paths) == 0 { 84 | paths = append(paths, "./...") 85 | } 86 | 87 | pkgs := LoadPackages(paths) 88 | n.Packages = pkgs 89 | 90 | if n.RunSSA { 91 | n.Logger.Info("Building SSA program") 92 | n.SSA = &SSA{ 93 | Packages: []*ssa.Package{}, 94 | } 95 | prog, ssaPkgs := ssautil.AllPackages(pkgs, ssa.InstantiateGenerics) 96 | n.SSA.Packages = ssaPkgs 97 | n.SSA.Program = prog 98 | prog.Build() 99 | 100 | n.Logger.Info("Generating SSA based callgraph", "alg", n.CallgraphAlg) 101 | switch n.CallgraphAlg { 102 | case "static": 103 | n.SSA.Callgraph = static.CallGraph(prog) 104 | case "cha": 105 | n.SSA.Callgraph = cha.CallGraph(prog) 106 | case "rta": 107 | mains := ssautil.MainPackages(ssaPkgs) 108 | var roots []*ssa.Function 109 | for _, main := range mains { 110 | roots = append(roots, main.Func("init"), main.Func("main")) 111 | } 112 | rtares := rta.Analyze(roots, true) 113 | n.SSA.Callgraph = rtares.CallGraph 114 | case "vta": 115 | n.SSA.Callgraph = vta.CallGraph(ssautil.AllFunctions(prog), cha.CallGraph(prog)) 116 | default: 117 | log.Fatalf("Unknown callgraph alg %s", n.CallgraphAlg) 118 | } 119 | n.Logger.Info("SSA callgraph generated successfully") 120 | } 121 | 122 | n.Logger.Info("Finding functions via AST parsing") 123 | // TODO: No real need to use ctrlflow.Analyzer if using SSA 124 | var analyzer = &analysis.Analyzer{ 125 | Name: "wally", 126 | Doc: "maps HTTP and RPC routes", 127 | Run: n.Run, 128 | Requires: []*analysis.Analyzer{inspect.Analyzer, ctrlflow.Analyzer, callermapper.Analyzer, tokenfile.Analyzer}, 129 | } 130 | 131 | wallyChecker := checker.InitChecker(analyzer) 132 | // TODO: consider this as part of a checker instead 133 | results := map[*analysis.Analyzer]interface{}{} 134 | for _, pkg := range pkgs { 135 | pkg := pkg 136 | pass := &analysis.Pass{ 137 | Analyzer: wallyChecker.Analyzer, 138 | Fset: pkg.Fset, 139 | Files: pkg.Syntax, 140 | OtherFiles: pkg.OtherFiles, 141 | IgnoredFiles: pkg.IgnoredFiles, 142 | Pkg: pkg.Types, 143 | TypesInfo: pkg.TypesInfo, 144 | TypesSizes: pkg.TypesSizes, 145 | ResultOf: results, 146 | Report: func(d analysis.Diagnostic) {}, 147 | ImportObjectFact: wallyChecker.ImportObjectFact, 148 | ExportObjectFact: wallyChecker.ExportObjectFact, 149 | ImportPackageFact: nil, 150 | ExportPackageFact: nil, 151 | AllObjectFacts: nil, 152 | AllPackageFacts: nil, 153 | } 154 | 155 | for _, a := range analyzer.Requires { 156 | res, err := a.Run(pass) 157 | if err != nil { 158 | n.Logger.Error("Error running analyzer %s: %s\n", wallyChecker.Analyzer.Name, err) 159 | continue 160 | } 161 | pass.ResultOf[a] = res 162 | } 163 | 164 | result, err := pass.Analyzer.Run(pass) 165 | if err != nil { 166 | n.Logger.Error("Error running analyzer %s: %s\n", wallyChecker.Analyzer.Name, err) 167 | continue 168 | } 169 | // This should be placed outside of this loop 170 | // we want to collect single results here, then run through all at the end. 171 | if result != nil { 172 | if passIssues, ok := result.([]match.RouteMatch); ok { 173 | n.RouteMatches = append(n.RouteMatches, passIssues...) 174 | } 175 | } 176 | } 177 | } 178 | 179 | func LoadPackages(paths []string) []*packages.Package { 180 | fset := token.NewFileSet() 181 | 182 | cfg := &packages.Config{ 183 | Mode: packages.NeedFiles | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | 184 | packages.NeedName | packages.NeedCompiledGoFiles | packages.NeedImports | 185 | packages.NeedExportFile | packages.NeedTypesSizes | packages.NeedModule | packages.NeedDeps, 186 | Fset: fset, 187 | } 188 | 189 | pkgs, err := packages.Load(cfg, paths...) 190 | if err != nil { 191 | fmt.Fprintf(os.Stderr, "load: %v\n", err) 192 | os.Exit(1) 193 | } 194 | 195 | return pkgs 196 | } 197 | 198 | func (n *Navigator) cacheVariables(node ast.Node, pass *analysis.Pass) { 199 | if genDcl, ok := node.(*ast.GenDecl); ok { 200 | n.RecordGlobals(genDcl, pass) 201 | } 202 | 203 | if dclStmt, ok := node.(*ast.DeclStmt); ok { 204 | if genDcl, ok := dclStmt.Decl.(*ast.GenDecl); ok { 205 | n.RecordGlobals(genDcl, pass) 206 | } 207 | } 208 | 209 | if stmt, ok := node.(*ast.AssignStmt); ok { 210 | n.RecordLocals(stmt, pass) 211 | } 212 | } 213 | 214 | func (n *Navigator) Run(pass *analysis.Pass) (interface{}, error) { 215 | inspecting := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 216 | callMapper := pass.ResultOf[callermapper.Analyzer].(*cefinder.CeFinder) 217 | //flow := pass.ResultOf[ctrlflow.Analyzer].(*ctrlflow.CFGs) 218 | 219 | nodeFilter := []ast.Node{ 220 | (*ast.CallExpr)(nil), 221 | (*ast.GenDecl)(nil), 222 | (*ast.AssignStmt)(nil), 223 | (*ast.DeclStmt)(nil), 224 | } 225 | 226 | var results []match.RouteMatch 227 | 228 | // this is basically the same as ast.Inspect(), only we don't return a 229 | // boolean anymore as it'll visit all the nodes based on the filter. 230 | inspecting.Preorder(nodeFilter, func(node ast.Node) { 231 | n.cacheVariables(node, pass) 232 | 233 | ce, ok := node.(*ast.CallExpr) 234 | if !ok { 235 | return 236 | } 237 | 238 | // We have a function if we have made it here 239 | funExpr := ce.Fun 240 | funcInfo, err := wallylib.GetFuncInfo(funExpr, pass.TypesInfo) 241 | if err != nil { 242 | return 243 | } 244 | 245 | // Get the position of the function in code 246 | pos := pass.Fset.Position(funExpr.Pos()) 247 | if !n.PassesExclusions(pos, funcInfo.Package) { 248 | return 249 | } 250 | 251 | // This will be used for funcInfo.Match 252 | decl := callMapper.EnclosingFunc(ce) 253 | if decl != nil { 254 | funcInfo.EnclosedBy = &wallylib.FuncDecl{ 255 | Pkg: pass.Pkg, 256 | Decl: decl, 257 | } 258 | } 259 | 260 | route := funcInfo.Match(n.RouteIndicators) 261 | if route == nil { 262 | // Don't keep going deeper in the node if there are no matches by now? 263 | return 264 | } 265 | 266 | // Whether we are able to get params or not we have a match 267 | funcMatch := match.NewRouteMatch(*route, pos) 268 | 269 | if modName := n.GetModuleName(funcInfo.Pkg); modName != "" { 270 | funcMatch.Module = modName 271 | } else { 272 | funcMatch.Module = n.GetModuleName(funcInfo.EnclosedBy.Pkg) 273 | } 274 | 275 | // Now try to get the params for methods, path, etc. 276 | funcMatch.Params = wallylib.ResolveParams(route.Params, funcInfo.Signature, ce, pass) 277 | 278 | //Get the enclosing func 279 | if n.RunSSA { 280 | ssapkg := n.SSAPkgFromTypesPackage(pass.Pkg) 281 | if ssapkg != nil { 282 | if ssaEnclosingFunc := GetEnclosingFuncWithSSA(pass, ce, ssapkg); ssaEnclosingFunc != nil { 283 | funcMatch.EnclosedBy = fmt.Sprintf("%s.%s", pass.Pkg.Name(), ssaEnclosingFunc.Name()) 284 | funcMatch.SSA.EnclosedByFunc = ssaEnclosingFunc 285 | funcMatch.SSA.SSAInstruction = n.GetCallInstructionFromSSAFunc(ssaEnclosingFunc, ce) 286 | 287 | if funcMatch.SSA.SSAInstruction != nil { 288 | funcMatch.SSA.SSAFunc = wallylib.GetFunctionFromCallInstruction(funcMatch.SSA.SSAInstruction) 289 | } else { 290 | n.Logger.Debug("unable to get SSA instruction for function", "function", ssaEnclosingFunc.Name()) 291 | } 292 | } 293 | } 294 | } 295 | 296 | if funcMatch.EnclosedBy == "" { 297 | if decl != nil { 298 | funcMatch.EnclosedBy = fmt.Sprintf("%s.%s", pass.Pkg.Name(), decl.Name.String()) 299 | } 300 | } 301 | 302 | results = append(results, funcMatch) 303 | }) 304 | 305 | return results, nil 306 | } 307 | 308 | func (n *Navigator) GetCallInstructionFromSSAFunc(enclosingFunc *ssa.Function, expr *ast.CallExpr) ssa.CallInstruction { 309 | for _, block := range enclosingFunc.Blocks { 310 | for _, instr := range block.Instrs { 311 | if call, ok := instr.(ssa.CallInstruction); ok { 312 | if n.isMatchingCall(call, expr) { 313 | return call 314 | } 315 | } 316 | } 317 | } 318 | 319 | return nil 320 | } 321 | 322 | func (n *Navigator) PassesExclusions(pos token.Position, pkg string) bool { 323 | if len(n.Exclusions.Packages) == 0 && len(n.Exclusions.PosSuffixes) == 0 { 324 | return true 325 | } 326 | 327 | for _, pkg := range n.Exclusions.Packages { 328 | if pkg == pkg { 329 | return false 330 | } 331 | } 332 | 333 | for _, exc := range n.Exclusions.PosSuffixes { 334 | if strings.HasSuffix(pos.String(), exc) { 335 | return false 336 | } 337 | } 338 | return true 339 | } 340 | 341 | func (n *Navigator) isMatchingCall(call ssa.CallInstruction, expr *ast.CallExpr) bool { 342 | var cp token.Pos 343 | if call.Value() == nil { 344 | cp = call.Common().Value.Pos() 345 | } else { 346 | cp = call.Value().Call.Value.Pos() 347 | } 348 | 349 | // Check with Lparem works for non-static calls 350 | if cp == expr.Pos() || call.Pos() == expr.Lparen { 351 | return true 352 | } 353 | return false 354 | } 355 | 356 | func (n *Navigator) GetCalledFunctionUsingEnclosing(enclosingFunc *ssa.Function, ce *ast.CallExpr) *ssa.Function { 357 | for _, block := range enclosingFunc.Blocks { 358 | for _, instr := range block.Instrs { 359 | if call, ok := instr.(*ssa.Call); ok { 360 | if call.Call.Pos() == ce.Pos() { 361 | if callee := call.Call.StaticCallee(); callee != nil { 362 | return callee 363 | } 364 | } 365 | } 366 | } 367 | } 368 | 369 | return nil 370 | } 371 | 372 | func (n *Navigator) SSAPkgFromTypesPackage(pkg *types.Package) *ssa.Package { 373 | for _, rpkg := range n.SSA.Packages { 374 | if rpkg != nil && rpkg.Pkg != nil { 375 | if rpkg.Pkg.String() == pkg.String() { 376 | return rpkg 377 | } 378 | } 379 | } 380 | return nil 381 | } 382 | 383 | // TODO: very slow function as it checks every node, one by one, and whether it has a path 384 | // to any of the matches. At the moment, not used and only prints results for testing 385 | func (n *Navigator) SolvePathsSlow() { 386 | for _, no := range n.SSA.Callgraph.Nodes { 387 | for _, routeMatch := range n.RouteMatches { 388 | edges := callgraph.PathSearch(no, func(node *callgraph.Node) bool { 389 | if node.Func != nil && node.Func == routeMatch.SSA.EnclosedByFunc { 390 | return true 391 | } else { 392 | return false 393 | } 394 | }) 395 | for _, s := range edges { 396 | fmt.Println("PATH IS: ", s.String()) 397 | } 398 | } 399 | } 400 | } 401 | 402 | func (n *Navigator) SolveCallPaths(options callmapper.Options) { 403 | var wg sync.WaitGroup 404 | 405 | for i, routeMatch := range n.RouteMatches { 406 | i, routeMatch := i, routeMatch 407 | 408 | if n.SSA.Callgraph.Nodes[routeMatch.SSA.EnclosedByFunc] == nil { 409 | continue 410 | } 411 | 412 | wg.Add(1) 413 | go func(i int, options callmapper.Options, routeMatch match.RouteMatch) { 414 | defer wg.Done() 415 | cm := callmapper.NewCallMapper(&routeMatch, n.SSA.Callgraph.Nodes, options) 416 | 417 | start := time.Now() 418 | n.Logger.Debug("Solving paths for match", "match", routeMatch.Pos.String()) 419 | 420 | if options.SearchAlg == callmapper.Dfs { 421 | n.RouteMatches[i].SSA.CallPaths = cm.AllPathsDFS(n.SSA.Callgraph.Nodes[routeMatch.SSA.EnclosedByFunc]) 422 | } else { 423 | n.RouteMatches[i].SSA.CallPaths = cm.AllPathsBFS(n.SSA.Callgraph.Nodes[routeMatch.SSA.EnclosedByFunc]) 424 | } 425 | 426 | duration := time.Since(start) 427 | n.Logger.Debug("Solved paths for match", "match", routeMatch.Pos.String(), "numPaths", len(n.RouteMatches[i].SSA.CallPaths.Paths), "duration", duration) 428 | }(i, options, routeMatch) 429 | } 430 | 431 | wg.Wait() 432 | } 433 | 434 | func (n *Navigator) RecordGlobals(gen *ast.GenDecl, pass *analysis.Pass) { 435 | for _, spec := range gen.Specs { 436 | s, ok := spec.(*ast.ValueSpec) 437 | if !ok { 438 | continue 439 | } 440 | 441 | for k, id := range s.Values { 442 | res := wallylib.GetValueFromExp(id, pass) 443 | if res == "" { 444 | continue 445 | } 446 | 447 | o1 := pass.TypesInfo.ObjectOf(s.Names[k]) 448 | if tt, ok := o1.(*types.Var); ok { 449 | // If same scope level as pkg 450 | if tt.Parent() == tt.Pkg().Scope() { 451 | // Scope level 452 | gv := new(checker.GlobalVar) 453 | gv.Val = res 454 | pass.ExportObjectFact(o1, gv) 455 | } 456 | } 457 | } 458 | } 459 | } 460 | 461 | func (n *Navigator) RecordLocals(gen *ast.AssignStmt, pass *analysis.Pass) { 462 | for idx, e := range gen.Rhs { 463 | idt, ok := gen.Lhs[idx].(*ast.Ident) 464 | if !ok { 465 | return 466 | } 467 | 468 | o1 := pass.TypesInfo.ObjectOf(idt) 469 | if !wallylib.IsLocal(o1) { 470 | return 471 | } 472 | 473 | res := wallylib.GetValueFromExp(e, pass) 474 | if res == "" || res == "\"\"" { 475 | return 476 | } 477 | 478 | var fact checker.LocalVar 479 | gv := new(checker.LocalVar) 480 | pass.ImportObjectFact(o1, &fact) 481 | 482 | if fact.Vals != nil { 483 | gv.Vals = fact.Vals 484 | gv.Vals = append(gv.Vals, res) 485 | pass.ExportObjectFact(o1, gv) 486 | 487 | } else { 488 | gv.Vals = append(gv.Vals, res) 489 | pass.ExportObjectFact(o1, gv) 490 | } 491 | } 492 | } 493 | 494 | func (n *Navigator) GetModuleName(typesPkg *types.Package) string { 495 | pkg := n.getPackagesPackageFromTypesPackage(typesPkg) 496 | // This will happen if the indicator given is for a standard library function 497 | // or if the project does not support modules. In such cases, for now, the user would have to specify a filter using the `-f` flag 498 | if pkg == nil { 499 | return "" 500 | } 501 | if pkg.Module != nil { 502 | return pkg.Module.Path 503 | } 504 | return "" 505 | } 506 | 507 | func (n *Navigator) getPackagesPackageFromTypesPackage(typesPkg *types.Package) *packages.Package { 508 | typesPkgPath := typesPkg.Path() 509 | for _, pkg := range n.Packages { 510 | if pkg.PkgPath == typesPkgPath { 511 | return pkg 512 | } 513 | } 514 | return nil 515 | } 516 | 517 | func GetObjFromCe(ce *ast.CallExpr, info *types.Info) types.Object { 518 | var funcObj types.Object 519 | 520 | switch fun := ce.Fun.(type) { 521 | case *ast.Ident: 522 | funcObj = info.ObjectOf(fun) 523 | case *ast.SelectorExpr: 524 | funcObj = info.ObjectOf(fun.Sel) 525 | default: 526 | return nil 527 | } 528 | 529 | return funcObj 530 | } 531 | 532 | func GetEnclosingFuncWithSSA(pass *analysis.Pass, ce *ast.CallExpr, ssaPkg *ssa.Package) *ssa.Function { 533 | currentFile := File(pass, ce.Fun.Pos()) 534 | ref, _ := astutil.PathEnclosingInterval(currentFile, ce.Pos(), ce.Pos()) 535 | return ssa.EnclosingFunction(ssaPkg, ref) 536 | } 537 | 538 | func File(pass *analysis.Pass, pos token.Pos) *ast.File { 539 | m := pass.ResultOf[tokenfile.Analyzer].(map[*token.File]*ast.File) 540 | return m[pass.Fset.File(pos)] 541 | } 542 | 543 | func (n *Navigator) PrintResults(format string, fileName string) { 544 | if format == "json" { 545 | if err := reporter.PrintJson(n.RouteMatches, fileName); err != nil { 546 | n.Logger.Error("Error printing to json", "error", err.Error()) 547 | } 548 | } else if format == "csv" { 549 | if err := reporter.WriteCSVFile(n.RouteMatches, fileName); err != nil { 550 | n.Logger.Error("Error printing CSV", "error", err.Error()) 551 | } 552 | } else { 553 | reporter.PrintResults(n.RouteMatches) 554 | } 555 | } 556 | -------------------------------------------------------------------------------- /passes/callermapper/callermapper.go: -------------------------------------------------------------------------------- 1 | package callermapper 2 | 3 | import ( 4 | "github.com/hex0punk/wally/passes/cefinder" 5 | match "github.com/hex0punk/wally/wallylib" 6 | "go/ast" 7 | "golang.org/x/tools/go/analysis" 8 | "golang.org/x/tools/go/analysis/passes/inspect" 9 | "golang.org/x/tools/go/ast/inspector" 10 | "reflect" 11 | ) 12 | 13 | var Analyzer = &analysis.Analyzer{ 14 | Name: "callermapper", 15 | Doc: "creates a mapping of func to ce's", 16 | Run: run, 17 | RunDespiteErrors: true, 18 | ResultType: reflect.TypeOf(new(cefinder.CeFinder)), 19 | } 20 | 21 | func run(pass *analysis.Pass) (interface{}, error) { 22 | inspecting := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 23 | nodeFilter := []ast.Node{ 24 | (*ast.FuncDecl)(nil), 25 | (*ast.GenDecl)(nil), 26 | } 27 | 28 | cf := cefinder.New() 29 | inspecting.Preorder(nodeFilter, func(node ast.Node) { 30 | fd, ok := node.(*ast.FuncDecl) 31 | if ok { 32 | // Go over the body of the function and get a map of all call expressions for later use 33 | if fd.Body != nil && fd.Body.List != nil { 34 | for _, b := range fd.Body.List { 35 | if ce := match.GetExprsFromStmt(b); len(ce) > 0 { 36 | cf.CE[fd] = append(cf.CE[fd], ce...) 37 | } 38 | } 39 | } 40 | } 41 | 42 | gd, ok := node.(*ast.FuncDecl) 43 | if !ok { 44 | return 45 | } 46 | 47 | if gd.Body != nil && gd.Body.List != nil { 48 | for _, b := range gd.Body.List { 49 | if ce := match.GetExprsFromStmt(b); len(ce) > 0 { 50 | cf.CE[gd] = append(cf.CE[gd], ce...) 51 | } 52 | } 53 | } 54 | 55 | }) 56 | return cf, nil 57 | } 58 | -------------------------------------------------------------------------------- /passes/cefinder/cefinder.go: -------------------------------------------------------------------------------- 1 | package cefinder 2 | 3 | import "go/ast" 4 | 5 | type CeFinder struct { 6 | CE map[*ast.FuncDecl][]*ast.CallExpr 7 | } 8 | 9 | func New() *CeFinder { 10 | return &CeFinder{ 11 | CE: make(map[*ast.FuncDecl][]*ast.CallExpr), 12 | } 13 | } 14 | 15 | func (finder *CeFinder) EnclosingFunc(ce *ast.CallExpr) *ast.FuncDecl { 16 | for dec, fun := range finder.CE { 17 | for _, ex := range fun { 18 | if ex == ce.Fun || ex == ce { 19 | return dec 20 | } 21 | } 22 | } 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /passes/tokenfile/tokenfile.go: -------------------------------------------------------------------------------- 1 | package tokenfile 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "reflect" 7 | 8 | "golang.org/x/tools/go/analysis" 9 | ) 10 | 11 | // code for this analyzeris taken directly from 12 | // https://github.com/dominikh/go-tools/blob/5447921adabdc6be434408ab8911a62fed3e0e52/analysis/facts/tokenfile/token.go 13 | var Analyzer = &analysis.Analyzer{ 14 | Name: "tokenfileanalyzer", 15 | Doc: "creates a mapping of *token.File to *ast.File", 16 | Run: func(pass *analysis.Pass) (interface{}, error) { 17 | m := map[*token.File]*ast.File{} 18 | for _, af := range pass.Files { 19 | tf := pass.Fset.File(af.Pos()) 20 | m[tf] = af 21 | } 22 | return m, nil 23 | }, 24 | RunDespiteErrors: true, 25 | ResultType: reflect.TypeOf(map[*token.File]*ast.File{}), 26 | } 27 | -------------------------------------------------------------------------------- /reporter/reporter.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "encoding/csv" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/goccy/go-graphviz" 8 | "github.com/goccy/go-graphviz/cgraph" 9 | "github.com/hex0punk/wally/match" 10 | "log" 11 | "os" 12 | "strings" 13 | ) 14 | 15 | func PrintResults(matches []match.RouteMatch) { 16 | for _, match := range matches { 17 | // TODO: This is printing the values from the indicator 18 | // That's fine, and it works but it should print values 19 | // from those captured during navigator, just in case 20 | PrintMach(match) 21 | } 22 | fmt.Println("Total Results: ", len(matches)) 23 | } 24 | 25 | func PrintMach(match match.RouteMatch) { 26 | fmt.Println("===========MATCH===============") 27 | fmt.Println("ID: ", match.MatchId) 28 | fmt.Println("Indicator ID: ", match.Indicator.Id) 29 | fmt.Println("Package: ", match.Indicator.Package) 30 | fmt.Println("Function: ", match.Indicator.Function) 31 | fmt.Println("Module: ", match.Module) 32 | fmt.Println("Params: ") 33 | for k, v := range match.Params { 34 | if v == "" { 35 | v = "" 36 | } 37 | if k == "" { 38 | k = "" 39 | } 40 | fmt.Printf(" %s: %s\n", k, v) 41 | } 42 | 43 | if match.SSA != nil && match.SSA.EnclosedByFunc != nil { 44 | fmt.Println("Enclosed by: ", match.SSA.EnclosedByFunc.String()) 45 | } else { 46 | fmt.Println("Enclosed by: ", match.EnclosedBy) 47 | } 48 | 49 | fmt.Printf("Position %s:%d\n", match.Pos.Filename, match.Pos.Line) 50 | if match.SSA != nil && match.SSA.CallPaths != nil && len(match.SSA.CallPaths.Paths) > 0 { 51 | if match.SSA.PathLimited { 52 | fmt.Println("Possible Paths (path limited):", len(match.SSA.CallPaths.Paths)) 53 | } else { 54 | fmt.Println("Possible Paths:", len(match.SSA.CallPaths.Paths)) 55 | } 56 | 57 | for i, paths := range match.SSA.CallPaths.Paths { 58 | fmt.Printf(" Path %d", i+1) 59 | if paths.NodeLimited { 60 | fmt.Printf(" (node limited)") 61 | } 62 | if paths.FilterLimited { 63 | fmt.Printf(" (filter limited)") 64 | } 65 | if paths.Recoverable { 66 | fmt.Printf(" (RECOVERABLE)") 67 | } 68 | fmt.Printf(":\n") 69 | 70 | for x := len(paths.Nodes) - 1; x >= 0; x-- { 71 | fmt.Printf(" %s --->\n", paths.Nodes[x].NodeString) 72 | } 73 | fmt.Printf(" %s\n", match.SSA.TargetPos) 74 | } 75 | } 76 | fmt.Println() 77 | } 78 | 79 | func GetJson(matches []match.RouteMatch) []byte { 80 | jsonOutput, err := json.Marshal(matches) 81 | if err != nil { 82 | log.Fatal(err) 83 | } 84 | 85 | return jsonOutput 86 | } 87 | 88 | func PrintJson(matches []match.RouteMatch, filename string) error { 89 | jsonOutput, err := json.Marshal(matches) 90 | if err != nil { 91 | fmt.Println(err) 92 | return err 93 | } 94 | 95 | if filename != "" { 96 | file, err := os.Create(filename) 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | defer file.Close() 101 | 102 | // Write data to the file 103 | _, err = file.Write(jsonOutput) 104 | if err != nil { 105 | log.Fatal(err) 106 | } 107 | } else { 108 | fmt.Println(string(jsonOutput)) 109 | } 110 | return nil 111 | } 112 | 113 | func WriteCSVFile(matches []match.RouteMatch, filePath string) error { 114 | file, err := os.Create(filePath) 115 | if err != nil { 116 | return fmt.Errorf("error creating file: %v", err) 117 | } 118 | defer file.Close() 119 | 120 | writer := csv.NewWriter(file) 121 | defer writer.Flush() 122 | 123 | // Writing the header of the CSV file 124 | if err := writer.Write([]string{"source", "target"}); err != nil { 125 | return fmt.Errorf("error writing header to CSV: %v", err) 126 | } 127 | 128 | for _, match := range matches { 129 | if match.SSA != nil && match.SSA.CallPaths != nil { 130 | for _, paths := range match.SSA.CallPaths.Paths { 131 | for i := 0; i < len(paths.Nodes)-1; i++ { 132 | if err := writer.Write([]string{paths.Nodes[i].NodeString, paths.Nodes[i+1].NodeString}); err != nil { 133 | return fmt.Errorf("error writing record to CSV: %v", err) 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | return nil 141 | } 142 | 143 | // TODO: Move this to a new package dedicated to graphing, or in this same package but in a separate file 144 | func GenerateGraph(matches []match.RouteMatch, path string) { 145 | g := graphviz.New() 146 | graph, err := g.Graph() 147 | if err != nil { 148 | log.Fatal(err) 149 | } 150 | for _, match := range matches { 151 | for _, paths := range match.SSA.CallPaths.Paths { 152 | var prev *cgraph.Node 153 | for i := 0; i < len(paths.Nodes); i++ { 154 | if i == 0 { 155 | prev, err = graph.CreateNode(paths.Nodes[i].NodeString) 156 | if err != nil { 157 | log.Fatal(err) 158 | } 159 | prev = prev.SetColor("red").SetFillColor("blue").SetShape("diamond") 160 | } else { 161 | newNode, err := graph.CreateNode(paths.Nodes[i].NodeString) 162 | if err != nil { 163 | log.Fatal(err) 164 | } 165 | _, err = graph.CreateEdge("e", newNode, prev) 166 | if err != nil { 167 | log.Fatal(err) 168 | } 169 | prev = newNode 170 | } 171 | } 172 | } 173 | } 174 | 175 | if strings.HasSuffix(path, ".png") { 176 | if err := g.RenderFilename(graph, graphviz.PNG, path); err != nil { 177 | log.Fatal(err) 178 | } 179 | } else if strings.HasSuffix(path, ".xdot") { 180 | if err := g.RenderFilename(graph, graphviz.XDOT, path); err != nil { 181 | log.Fatal(err) 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /sampleapp/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hex0punk/wally/sampleapp 2 | 3 | go 1.22.4 4 | -------------------------------------------------------------------------------- /sampleapp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hex0punk/wally/sampleapp/printer" 6 | "github.com/hex0punk/wally/sampleapp/safe" 7 | ) 8 | 9 | func main() { 10 | word := "Hello" 11 | idx := 7 12 | printCharSafe(word, idx) 13 | printChar(word, idx) 14 | RunAll(word, idx) 15 | ra := RunAll 16 | ra(word, idx) 17 | } 18 | 19 | func RunAll(str string, idx int) { 20 | printCharSafe(str, idx) 21 | printer.PrintOrPanic(str, idx) 22 | testF := printCharSafe 23 | testF(str, idx) 24 | } 25 | 26 | func ThisIsACall(str string) { 27 | fmt.Println(str) 28 | } 29 | func printCharSafe(word string, idx int) { 30 | safe.RunSafely( 31 | func() { 32 | printer.PrintOrPanic(word, idx) 33 | }) 34 | } 35 | 36 | func printChar(word string, idx int) { 37 | ThisIsACall("HOOOOLA") 38 | //printer.PrintOrPanic(word, idx) 39 | } 40 | -------------------------------------------------------------------------------- /sampleapp/printer/printer.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import "fmt" 4 | 5 | func PrintOrPanic(word string, idx int) { 6 | letter := word[idx] 7 | fmt.Println("letter is ", letter) 8 | } 9 | -------------------------------------------------------------------------------- /sampleapp/safe/safe.go: -------------------------------------------------------------------------------- 1 | package safe 2 | 3 | import "fmt" 4 | 5 | func RunSafely(fn func()) { 6 | defer func() { 7 | if recovered := recover(); recovered != nil { 8 | fmt.Printf("recovered by safe.Wrap - %v\r\n", fn) 9 | return 10 | } 11 | }() 12 | fn() 13 | } 14 | -------------------------------------------------------------------------------- /server/dist/browser.a9eca276.js: -------------------------------------------------------------------------------- 1 | (0,globalThis.parcelRequire3352.register)("fvaAM",function(e,r){e.exports=function(){throw Error("ws does not work in the browser. Browser clients must use the native WebSocket object")}}); 2 | //# sourceMappingURL=browser.a9eca276.js.map 3 | -------------------------------------------------------------------------------- /server/dist/browser.a9eca276.js.map: -------------------------------------------------------------------------------- 1 | {"mappings":"A,C,E,A,A,W,iB,C,Q,A,E,Q,S,C,C,C,ECEA,EAAA,OAAA,CAAiB,WACf,MAAM,AAAI,MACR,wFAGJ,C","sources":["","node_modules/ws/browser.js"],"sourcesContent":["\n var $parcel$global = globalThis;\n var parcelRequire = $parcel$global[\"parcelRequire3352\"];\nvar parcelRegister = parcelRequire.register;\nparcelRegister(\"fvaAM\", function(module, exports) {\n\"use strict\";\nmodule.exports = function() {\n throw new Error(\"ws does not work in the browser. Browser clients must use the native WebSocket object\");\n};\n\n});\n\n\n//# sourceMappingURL=browser.a9eca276.js.map\n","'use strict';\n\nmodule.exports = function () {\n throw new Error(\n 'ws does not work in the browser. Browser clients must use the native ' +\n 'WebSocket object'\n );\n};\n"],"names":["parcelRequire","$parcel$global","globalThis","register","module","exports","Error"],"version":3,"file":"browser.a9eca276.js.map"} -------------------------------------------------------------------------------- /server/dist/index.e5f1a8e6.css: -------------------------------------------------------------------------------- 1 | *,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}:before,:after{--tw-content:""}html,:host{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{line-height:inherit;margin:0}hr{color:inherit;border-top-width:1px;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-feature-settings:normal;font-variation-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-feature-settings:inherit;font-variation-settings:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder{opacity:1;color:#9ca3af}textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.invisible{visibility:hidden}.absolute{position:absolute}.relative{position:relative}.right-0{right:0}.top-0{top:0}.z-0{z-index:0}.z-10{z-index:10}.mb-4{margin-bottom:1rem}.block{display:block}.flex{display:flex}.h-screen{height:100vh}.w-64{width:16rem}.w-full{width:100%}.flex-1{flex:1}.flex-none{flex:none}.flex-row{flex-direction:row}.space-x-1>:not([hidden])~:not([hidden]){margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*calc(1 - var(--tw-space-x-reverse)));--tw-space-x-reverse:0}.space-y-2>:not([hidden])~:not([hidden]){margin-top:calc(.5rem*calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse));--tw-space-y-reverse:0}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.bg-gray-100{background-color:rgb(243 244 246/var(--tw-bg-opacity));--tw-bg-opacity:1}.bg-gray-800{background-color:rgb(31 41 55/var(--tw-bg-opacity));--tw-bg-opacity:1}.bg-gray-900{background-color:rgb(17 24 39/var(--tw-bg-opacity));--tw-bg-opacity:1}.bg-transparent{background-color:#0000}.p-4{padding:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.text-center{text-align:center}.text-right{text-align:right}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.text-white{color:rgb(255 255 255/var(--tw-text-opacity));--tw-text-opacity:1}.hover\:bg-gray-700:hover,.focus\:bg-gray-700:focus{background-color:rgb(55 65 81/var(--tw-bg-opacity));--tw-bg-opacity:1}.focus\:outline-none:focus{outline-offset:2px;outline:2px solid #0000} 2 | /*# sourceMappingURL=index.e5f1a8e6.css.map */ 3 | -------------------------------------------------------------------------------- /server/dist/index.e5f1a8e6.css.map: -------------------------------------------------------------------------------- 1 | {"mappings":"ACSA,8DAaA,+BAeA,yRA2BA,kCAaA,+CAaA,8FASA,wDAcA,wGASA,4BAYA,gMAmBA,oBAQA,8EAQA,kBAIA,cAUA,kEAeA,gNA6BA,kCAUA,uHAgBA,6BAQA,iCAQA,iCAQA,wCAAA,wCAUA,+DAWA,oDASA,oEAWA,0BAQA,4DAgBA,4BAKA,iBAIA,8CAYA,iBAQA,yBASA,gDAAA,mDAQA,iEAYA,oCASA,yBAUA,mFAkBA,qCAQA,sBAIA,0gCAsGA,6BAIA,4BAIA,4BAIA,iBAIA,aAIA,eAIA,iBAIA,yBAIA,qBAIA,mBAIA,uBAIA,kBAIA,mBAIA,eAIA,qBAIA,6BAIA,iLAMA,+KAMA,sCAIA,8BAIA,sFAKA,mFAKA,mFAKA,uCAIA,kBAIA,2CAKA,6CAKA,+BAIA,6BAIA,4CAKA,gDAKA,+CAKA,2CAKA,2BAIA,+BAIA,8EASA,0HAUA","sources":["index.e5f1a8e6.css","src/styles/output.css"],"sourcesContent":["*, :before, :after {\n box-sizing: border-box;\n border: 0 solid #e5e7eb;\n}\n\n:before, :after {\n --tw-content: \"\";\n}\n\nhtml, :host {\n -webkit-text-size-adjust: 100%;\n tab-size: 4;\n font-feature-settings: normal;\n font-variation-settings: normal;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;\n line-height: 1.5;\n}\n\nbody {\n line-height: inherit;\n margin: 0;\n}\n\nhr {\n color: inherit;\n border-top-width: 1px;\n height: 0;\n}\n\nabbr:where([title]) {\n text-decoration: underline dotted;\n}\n\nh1, h2, h3, h4, h5, h6 {\n font-size: inherit;\n font-weight: inherit;\n}\n\na {\n color: inherit;\n -webkit-text-decoration: inherit;\n text-decoration: inherit;\n}\n\nb, strong {\n font-weight: bolder;\n}\n\ncode, kbd, samp, pre {\n font-feature-settings: normal;\n font-variation-settings: normal;\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;\n font-size: 1em;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub, sup {\n vertical-align: baseline;\n font-size: 75%;\n line-height: 0;\n position: relative;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\ntable {\n text-indent: 0;\n border-color: inherit;\n border-collapse: collapse;\n}\n\nbutton, input, optgroup, select, textarea {\n font-feature-settings: inherit;\n font-variation-settings: inherit;\n font-family: inherit;\n font-size: 100%;\n font-weight: inherit;\n line-height: inherit;\n color: inherit;\n margin: 0;\n padding: 0;\n}\n\nbutton, select {\n text-transform: none;\n}\n\nbutton, [type=\"button\"], [type=\"reset\"], [type=\"submit\"] {\n -webkit-appearance: button;\n background-color: #0000;\n background-image: none;\n}\n\n:-moz-focusring {\n outline: auto;\n}\n\n:-moz-ui-invalid {\n box-shadow: none;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n::-webkit-inner-spin-button {\n height: auto;\n}\n\n::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n -webkit-appearance: textfield;\n outline-offset: -2px;\n}\n\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n -webkit-appearance: button;\n font: inherit;\n}\n\nsummary {\n display: list-item;\n}\n\nblockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre {\n margin: 0;\n}\n\nfieldset {\n margin: 0;\n padding: 0;\n}\n\nlegend {\n padding: 0;\n}\n\nol, ul, menu {\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\ndialog {\n padding: 0;\n}\n\ntextarea {\n resize: vertical;\n}\n\ninput::-moz-placeholder {\n opacity: 1;\n color: #9ca3af;\n}\n\ntextarea::-moz-placeholder {\n opacity: 1;\n color: #9ca3af;\n}\n\ninput::placeholder, textarea::placeholder {\n opacity: 1;\n color: #9ca3af;\n}\n\nbutton, [role=\"button\"] {\n cursor: pointer;\n}\n\n:disabled {\n cursor: default;\n}\n\nimg, svg, video, canvas, audio, iframe, embed, object {\n vertical-align: middle;\n display: block;\n}\n\nimg, video {\n max-width: 100%;\n height: auto;\n}\n\n[hidden] {\n display: none;\n}\n\n*, :before, :after, ::backdrop {\n --tw-border-spacing-x: 0;\n --tw-border-spacing-y: 0;\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-rotate: 0;\n --tw-skew-x: 0;\n --tw-skew-y: 0;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-pan-x: ;\n --tw-pan-y: ;\n --tw-pinch-zoom: ;\n --tw-scroll-snap-strictness: proximity;\n --tw-gradient-from-position: ;\n --tw-gradient-via-position: ;\n --tw-gradient-to-position: ;\n --tw-ordinal: ;\n --tw-slashed-zero: ;\n --tw-numeric-figure: ;\n --tw-numeric-spacing: ;\n --tw-numeric-fraction: ;\n --tw-ring-inset: ;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-color: #3b82f680;\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-ring-shadow: 0 0 #0000;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-colored: 0 0 #0000;\n --tw-blur: ;\n --tw-brightness: ;\n --tw-contrast: ;\n --tw-grayscale: ;\n --tw-hue-rotate: ;\n --tw-invert: ;\n --tw-saturate: ;\n --tw-sepia: ;\n --tw-drop-shadow: ;\n --tw-backdrop-blur: ;\n --tw-backdrop-brightness: ;\n --tw-backdrop-contrast: ;\n --tw-backdrop-grayscale: ;\n --tw-backdrop-hue-rotate: ;\n --tw-backdrop-invert: ;\n --tw-backdrop-opacity: ;\n --tw-backdrop-saturate: ;\n --tw-backdrop-sepia: ;\n}\n\n.invisible {\n visibility: hidden;\n}\n\n.absolute {\n position: absolute;\n}\n\n.relative {\n position: relative;\n}\n\n.right-0 {\n right: 0;\n}\n\n.top-0 {\n top: 0;\n}\n\n.z-0 {\n z-index: 0;\n}\n\n.z-10 {\n z-index: 10;\n}\n\n.mb-4 {\n margin-bottom: 1rem;\n}\n\n.block {\n display: block;\n}\n\n.flex {\n display: flex;\n}\n\n.h-screen {\n height: 100vh;\n}\n\n.w-64 {\n width: 16rem;\n}\n\n.w-full {\n width: 100%;\n}\n\n.flex-1 {\n flex: 1;\n}\n\n.flex-none {\n flex: none;\n}\n\n.flex-row {\n flex-direction: row;\n}\n\n.space-x-1 > :not([hidden]) ~ :not([hidden]) {\n margin-right: calc(.25rem * var(--tw-space-x-reverse));\n margin-left: calc(.25rem * calc(1 - var(--tw-space-x-reverse)));\n --tw-space-x-reverse: 0;\n}\n\n.space-y-2 > :not([hidden]) ~ :not([hidden]) {\n margin-top: calc(.5rem * calc(1 - var(--tw-space-y-reverse)));\n margin-bottom: calc(.5rem * var(--tw-space-y-reverse));\n --tw-space-y-reverse: 0;\n}\n\n.break-words {\n overflow-wrap: break-word;\n}\n\n.rounded {\n border-radius: .25rem;\n}\n\n.bg-gray-100 {\n background-color: rgb(243 244 246 / var(--tw-bg-opacity));\n --tw-bg-opacity: 1;\n}\n\n.bg-gray-800 {\n background-color: rgb(31 41 55 / var(--tw-bg-opacity));\n --tw-bg-opacity: 1;\n}\n\n.bg-gray-900 {\n background-color: rgb(17 24 39 / var(--tw-bg-opacity));\n --tw-bg-opacity: 1;\n}\n\n.bg-transparent {\n background-color: #0000;\n}\n\n.p-4 {\n padding: 1rem;\n}\n\n.px-4 {\n padding-left: 1rem;\n padding-right: 1rem;\n}\n\n.py-2 {\n padding-top: .5rem;\n padding-bottom: .5rem;\n}\n\n.text-center {\n text-align: center;\n}\n\n.text-right {\n text-align: right;\n}\n\n.text-2xl {\n font-size: 1.5rem;\n line-height: 2rem;\n}\n\n.text-lg {\n font-size: 1.125rem;\n line-height: 1.75rem;\n}\n\n.text-sm {\n font-size: .875rem;\n line-height: 1.25rem;\n}\n\n.text-xs {\n font-size: .75rem;\n line-height: 1rem;\n}\n\n.font-bold {\n font-weight: 700;\n}\n\n.font-semibold {\n font-weight: 600;\n}\n\n.text-white {\n color: rgb(255 255 255 / var(--tw-text-opacity));\n --tw-text-opacity: 1;\n}\n\n.hover\\:bg-gray-700:hover, .focus\\:bg-gray-700:focus {\n background-color: rgb(55 65 81 / var(--tw-bg-opacity));\n --tw-bg-opacity: 1;\n}\n\n.focus\\:outline-none:focus {\n outline-offset: 2px;\n outline: 2px solid #0000;\n}\n/*# sourceMappingURL=index.e5f1a8e6.css.map */\n","/*\n! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com\n*/\n\n/*\n1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)\n2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)\n*/\n\n*,\n::before,\n::after {\n box-sizing: border-box;\n /* 1 */\n border-width: 0;\n /* 2 */\n border-style: solid;\n /* 2 */\n border-color: #e5e7eb;\n /* 2 */\n}\n\n::before,\n::after {\n --tw-content: \"\";\n}\n\n/*\n1. Use a consistent sensible line-height in all browsers.\n2. Prevent adjustments of font size after orientation changes in iOS.\n3. Use a more readable tab size.\n4. Use the user's configured `sans` font-family by default.\n5. Use the user's configured `sans` font-feature-settings by default.\n6. Use the user's configured `sans` font-variation-settings by default.\n7. Disable tap highlights on iOS\n*/\n\nhtml,\n:host {\n line-height: 1.5;\n /* 1 */\n -webkit-text-size-adjust: 100%;\n /* 2 */\n -moz-tab-size: 4;\n /* 3 */\n -o-tab-size: 4;\n tab-size: 4;\n /* 3 */\n font-family: ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\",\n \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n /* 4 */\n font-feature-settings: normal;\n /* 5 */\n font-variation-settings: normal;\n /* 6 */\n -webkit-tap-highlight-color: transparent;\n /* 7 */\n}\n\n/*\n1. Remove the margin in all browsers.\n2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.\n*/\n\nbody {\n margin: 0;\n /* 1 */\n line-height: inherit;\n /* 2 */\n}\n\n/*\n1. Add the correct height in Firefox.\n2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\n3. Ensure horizontal rules are visible by default.\n*/\n\nhr {\n height: 0;\n /* 1 */\n color: inherit;\n /* 2 */\n border-top-width: 1px;\n /* 3 */\n}\n\n/*\nAdd the correct text decoration in Chrome, Edge, and Safari.\n*/\n\nabbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n}\n\n/*\nRemove the default font size and weight for headings.\n*/\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n font-size: inherit;\n font-weight: inherit;\n}\n\n/*\nReset links to optimize for opt-in styling instead of opt-out.\n*/\n\na {\n color: inherit;\n text-decoration: inherit;\n}\n\n/*\nAdd the correct font weight in Edge and Safari.\n*/\n\nb,\nstrong {\n font-weight: bolder;\n}\n\n/*\n1. Use the user's configured `mono` font-family by default.\n2. Use the user's configured `mono` font-feature-settings by default.\n3. Use the user's configured `mono` font-variation-settings by default.\n4. Correct the odd `em` font sizing in all browsers.\n*/\n\ncode,\nkbd,\nsamp,\npre {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,\n \"Liberation Mono\", \"Courier New\", monospace;\n /* 1 */\n font-feature-settings: normal;\n /* 2 */\n font-variation-settings: normal;\n /* 3 */\n font-size: 1em;\n /* 4 */\n}\n\n/*\nAdd the correct font size in all browsers.\n*/\n\nsmall {\n font-size: 80%;\n}\n\n/*\nPrevent `sub` and `sup` elements from affecting the line height in all browsers.\n*/\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\n/*\n1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\n2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\n3. Remove gaps between table borders by default.\n*/\n\ntable {\n text-indent: 0;\n /* 1 */\n border-color: inherit;\n /* 2 */\n border-collapse: collapse;\n /* 3 */\n}\n\n/*\n1. Change the font styles in all browsers.\n2. Remove the margin in Firefox and Safari.\n3. Remove default padding in all browsers.\n*/\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n font-family: inherit;\n /* 1 */\n font-feature-settings: inherit;\n /* 1 */\n font-variation-settings: inherit;\n /* 1 */\n font-size: 100%;\n /* 1 */\n font-weight: inherit;\n /* 1 */\n line-height: inherit;\n /* 1 */\n color: inherit;\n /* 1 */\n margin: 0;\n /* 2 */\n padding: 0;\n /* 3 */\n}\n\n/*\nRemove the inheritance of text transform in Edge and Firefox.\n*/\n\nbutton,\nselect {\n text-transform: none;\n}\n\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Remove default button styles.\n*/\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n /* 1 */\n background-color: transparent;\n /* 2 */\n background-image: none;\n /* 2 */\n}\n\n/*\nUse the modern Firefox focus style for all focusable elements.\n*/\n\n:-moz-focusring {\n outline: auto;\n}\n\n/*\nRemove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)\n*/\n\n:-moz-ui-invalid {\n box-shadow: none;\n}\n\n/*\nAdd the correct vertical alignment in Chrome and Firefox.\n*/\n\nprogress {\n vertical-align: baseline;\n}\n\n/*\nCorrect the cursor style of increment and decrement buttons in Safari.\n*/\n\n::-webkit-inner-spin-button,\n::-webkit-outer-spin-button {\n height: auto;\n}\n\n/*\n1. Correct the odd appearance in Chrome and Safari.\n2. Correct the outline style in Safari.\n*/\n\n[type=\"search\"] {\n -webkit-appearance: textfield;\n /* 1 */\n outline-offset: -2px;\n /* 2 */\n}\n\n/*\nRemove the inner padding in Chrome and Safari on macOS.\n*/\n\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Change font properties to `inherit` in Safari.\n*/\n\n::-webkit-file-upload-button {\n -webkit-appearance: button;\n /* 1 */\n font: inherit;\n /* 2 */\n}\n\n/*\nAdd the correct display in Chrome and Safari.\n*/\n\nsummary {\n display: list-item;\n}\n\n/*\nRemoves the default spacing and border for appropriate elements.\n*/\n\nblockquote,\ndl,\ndd,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nhr,\nfigure,\np,\npre {\n margin: 0;\n}\n\nfieldset {\n margin: 0;\n padding: 0;\n}\n\nlegend {\n padding: 0;\n}\n\nol,\nul,\nmenu {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n/*\nReset default styling for dialogs.\n*/\n\ndialog {\n padding: 0;\n}\n\n/*\nPrevent resizing textareas horizontally by default.\n*/\n\ntextarea {\n resize: vertical;\n}\n\n/*\n1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)\n2. Set the default placeholder color to the user's configured gray 400 color.\n*/\n\ninput::-moz-placeholder,\ntextarea::-moz-placeholder {\n opacity: 1;\n /* 1 */\n color: #9ca3af;\n /* 2 */\n}\n\ninput::placeholder,\ntextarea::placeholder {\n opacity: 1;\n /* 1 */\n color: #9ca3af;\n /* 2 */\n}\n\n/*\nSet the default cursor for buttons.\n*/\n\nbutton,\n[role=\"button\"] {\n cursor: pointer;\n}\n\n/*\nMake sure disabled buttons don't get the pointer cursor.\n*/\n\n:disabled {\n cursor: default;\n}\n\n/*\n1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)\n2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)\n This can trigger a poorly considered lint error in some tools but is included by design.\n*/\n\nimg,\nsvg,\nvideo,\ncanvas,\naudio,\niframe,\nembed,\nobject {\n display: block;\n /* 1 */\n vertical-align: middle;\n /* 2 */\n}\n\n/*\nConstrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)\n*/\n\nimg,\nvideo {\n max-width: 100%;\n height: auto;\n}\n\n/* Make elements with the HTML hidden attribute stay hidden by default */\n\n[hidden] {\n display: none;\n}\n\n*,\n::before,\n::after {\n --tw-border-spacing-x: 0;\n --tw-border-spacing-y: 0;\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-rotate: 0;\n --tw-skew-x: 0;\n --tw-skew-y: 0;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-pan-x: ;\n --tw-pan-y: ;\n --tw-pinch-zoom: ;\n --tw-scroll-snap-strictness: proximity;\n --tw-gradient-from-position: ;\n --tw-gradient-via-position: ;\n --tw-gradient-to-position: ;\n --tw-ordinal: ;\n --tw-slashed-zero: ;\n --tw-numeric-figure: ;\n --tw-numeric-spacing: ;\n --tw-numeric-fraction: ;\n --tw-ring-inset: ;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-color: rgb(59 130 246 / 0.5);\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-ring-shadow: 0 0 #0000;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-colored: 0 0 #0000;\n --tw-blur: ;\n --tw-brightness: ;\n --tw-contrast: ;\n --tw-grayscale: ;\n --tw-hue-rotate: ;\n --tw-invert: ;\n --tw-saturate: ;\n --tw-sepia: ;\n --tw-drop-shadow: ;\n --tw-backdrop-blur: ;\n --tw-backdrop-brightness: ;\n --tw-backdrop-contrast: ;\n --tw-backdrop-grayscale: ;\n --tw-backdrop-hue-rotate: ;\n --tw-backdrop-invert: ;\n --tw-backdrop-opacity: ;\n --tw-backdrop-saturate: ;\n --tw-backdrop-sepia: ;\n}\n\n::backdrop {\n --tw-border-spacing-x: 0;\n --tw-border-spacing-y: 0;\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-rotate: 0;\n --tw-skew-x: 0;\n --tw-skew-y: 0;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-pan-x: ;\n --tw-pan-y: ;\n --tw-pinch-zoom: ;\n --tw-scroll-snap-strictness: proximity;\n --tw-gradient-from-position: ;\n --tw-gradient-via-position: ;\n --tw-gradient-to-position: ;\n --tw-ordinal: ;\n --tw-slashed-zero: ;\n --tw-numeric-figure: ;\n --tw-numeric-spacing: ;\n --tw-numeric-fraction: ;\n --tw-ring-inset: ;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-color: rgb(59 130 246 / 0.5);\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-ring-shadow: 0 0 #0000;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-colored: 0 0 #0000;\n --tw-blur: ;\n --tw-brightness: ;\n --tw-contrast: ;\n --tw-grayscale: ;\n --tw-hue-rotate: ;\n --tw-invert: ;\n --tw-saturate: ;\n --tw-sepia: ;\n --tw-drop-shadow: ;\n --tw-backdrop-blur: ;\n --tw-backdrop-brightness: ;\n --tw-backdrop-contrast: ;\n --tw-backdrop-grayscale: ;\n --tw-backdrop-hue-rotate: ;\n --tw-backdrop-invert: ;\n --tw-backdrop-opacity: ;\n --tw-backdrop-saturate: ;\n --tw-backdrop-sepia: ;\n}\n\n.invisible {\n visibility: hidden;\n}\n\n.absolute {\n position: absolute;\n}\n\n.relative {\n position: relative;\n}\n\n.right-0 {\n right: 0px;\n}\n\n.top-0 {\n top: 0px;\n}\n\n.z-0 {\n z-index: 0;\n}\n\n.z-10 {\n z-index: 10;\n}\n\n.mb-4 {\n margin-bottom: 1rem;\n}\n\n.block {\n display: block;\n}\n\n.flex {\n display: flex;\n}\n\n.h-screen {\n height: 100vh;\n}\n\n.w-64 {\n width: 16rem;\n}\n\n.w-full {\n width: 100%;\n}\n\n.flex-1 {\n flex: 1 1 0%;\n}\n\n.flex-none {\n flex: none;\n}\n\n.flex-row {\n flex-direction: row;\n}\n\n.space-x-1 > :not([hidden]) ~ :not([hidden]) {\n --tw-space-x-reverse: 0;\n margin-right: calc(0.25rem * var(--tw-space-x-reverse));\n margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse)));\n}\n\n.space-y-2 > :not([hidden]) ~ :not([hidden]) {\n --tw-space-y-reverse: 0;\n margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));\n margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));\n}\n\n.break-words {\n overflow-wrap: break-word;\n}\n\n.rounded {\n border-radius: 0.25rem;\n}\n\n.bg-gray-100 {\n --tw-bg-opacity: 1;\n background-color: rgb(243 244 246 / var(--tw-bg-opacity));\n}\n\n.bg-gray-800 {\n --tw-bg-opacity: 1;\n background-color: rgb(31 41 55 / var(--tw-bg-opacity));\n}\n\n.bg-gray-900 {\n --tw-bg-opacity: 1;\n background-color: rgb(17 24 39 / var(--tw-bg-opacity));\n}\n\n.bg-transparent {\n background-color: transparent;\n}\n\n.p-4 {\n padding: 1rem;\n}\n\n.px-4 {\n padding-left: 1rem;\n padding-right: 1rem;\n}\n\n.py-2 {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n\n.text-center {\n text-align: center;\n}\n\n.text-right {\n text-align: right;\n}\n\n.text-2xl {\n font-size: 1.5rem;\n line-height: 2rem;\n}\n\n.text-lg {\n font-size: 1.125rem;\n line-height: 1.75rem;\n}\n\n.text-sm {\n font-size: 0.875rem;\n line-height: 1.25rem;\n}\n\n.text-xs {\n font-size: 0.75rem;\n line-height: 1rem;\n}\n\n.font-bold {\n font-weight: 700;\n}\n\n.font-semibold {\n font-weight: 600;\n}\n\n.text-white {\n --tw-text-opacity: 1;\n color: rgb(255 255 255 / var(--tw-text-opacity));\n}\n\n/* .invisible {\n visibility: hidden;\n} */\n\n.hover\\:bg-gray-700:hover {\n --tw-bg-opacity: 1;\n background-color: rgb(55 65 81 / var(--tw-bg-opacity));\n}\n\n.focus\\:bg-gray-700:focus {\n --tw-bg-opacity: 1;\n background-color: rgb(55 65 81 / var(--tw-bg-opacity));\n}\n\n.focus\\:outline-none:focus {\n outline: 2px solid transparent;\n outline-offset: 2px;\n}\n"],"names":[],"version":3,"file":"index.e5f1a8e6.css.map"} -------------------------------------------------------------------------------- /server/dist/index.html: -------------------------------------------------------------------------------- 1 | Wally

Wally

-------------------------------------------------------------------------------- /server/dist/index.runtime.ef08fcd2.js: -------------------------------------------------------------------------------- 1 | function e(e,r,t,n){Object.defineProperty(e,r,{get:t,set:n,enumerable:!0,configurable:!0})}var r=globalThis,t={},n={},o=r.parcelRequire3352;null==o&&((o=function(e){if(e in t)return t[e].exports;if(e in n){var r=n[e];delete n[e];var o={id:e,exports:{}};return t[e]=o,r.call(o.exports,o,o.exports),o.exports}var i=Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,r){n[e]=r},r.parcelRequire3352=o),(0,o.register)("27Lyk",function(r,t){e(r.exports,"register",()=>n,e=>n=e),e(r.exports,"resolve",()=>o,e=>o=e);var n,o,i=new Map;n=function(e,r){for(var t=0;t","node_modules/@parcel/runtime-js/lib/helpers/bundle-manifest.js","node_modules/@parcel/runtime-js/lib/runtime-acb420d8559ff5c0.js"],"sourcesContent":["\nfunction $parcel$export(e, n, v, s) {\n Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true});\n}\n\n var $parcel$global = globalThis;\n \nvar $parcel$modules = {};\nvar $parcel$inits = {};\n\nvar parcelRequire = $parcel$global[\"parcelRequire3352\"];\n\nif (parcelRequire == null) {\n parcelRequire = function(id) {\n if (id in $parcel$modules) {\n return $parcel$modules[id].exports;\n }\n if (id in $parcel$inits) {\n var init = $parcel$inits[id];\n delete $parcel$inits[id];\n var module = {id: id, exports: {}};\n $parcel$modules[id] = module;\n init.call(module.exports, module, module.exports);\n return module.exports;\n }\n var err = new Error(\"Cannot find module '\" + id + \"'\");\n err.code = 'MODULE_NOT_FOUND';\n throw err;\n };\n\n parcelRequire.register = function register(id, init) {\n $parcel$inits[id] = init;\n };\n\n $parcel$global[\"parcelRequire3352\"] = parcelRequire;\n}\n\nvar parcelRegister = parcelRequire.register;\nparcelRegister(\"27Lyk\", function(module, exports) {\n\n$parcel$export(module.exports, \"register\", () => $18c11f3350a906ea$export$6503ec6e8aabbaf, (v) => $18c11f3350a906ea$export$6503ec6e8aabbaf = v);\n$parcel$export(module.exports, \"resolve\", () => $18c11f3350a906ea$export$f7ad0328861e2f03, (v) => $18c11f3350a906ea$export$f7ad0328861e2f03 = v);\nvar $18c11f3350a906ea$export$6503ec6e8aabbaf;\nvar $18c11f3350a906ea$export$f7ad0328861e2f03;\n\"use strict\";\nvar $18c11f3350a906ea$var$mapping = new Map();\nfunction $18c11f3350a906ea$var$register(baseUrl, manifest) {\n for(var i = 0; i < manifest.length - 1; i += 2)$18c11f3350a906ea$var$mapping.set(manifest[i], {\n baseUrl: baseUrl,\n path: manifest[i + 1]\n });\n}\nfunction $18c11f3350a906ea$var$resolve(id) {\n var resolved = $18c11f3350a906ea$var$mapping.get(id);\n if (resolved == null) throw new Error(\"Could not resolve bundle with id \" + id);\n return new URL(resolved.path, resolved.baseUrl).toString();\n}\n$18c11f3350a906ea$export$6503ec6e8aabbaf = $18c11f3350a906ea$var$register;\n$18c11f3350a906ea$export$f7ad0328861e2f03 = $18c11f3350a906ea$var$resolve;\n\n});\n\nvar $e5d65878cea9fe5a$exports = {};\n\n(parcelRequire(\"27Lyk\")).register(new URL(\"\", import.meta.url).toString(), JSON.parse('[\"1LzKV\",\"index.585f762b.js\",\"kibx1\",\"browser.a9eca276.js\"]'));\n\n\n//# sourceMappingURL=index.runtime.ef08fcd2.js.map\n","\"use strict\";\n\nvar mapping = new Map();\nfunction register(baseUrl, manifest) {\n for (var i = 0; i < manifest.length - 1; i += 2) {\n mapping.set(manifest[i], {\n baseUrl: baseUrl,\n path: manifest[i + 1]\n });\n }\n}\nfunction resolve(id) {\n var resolved = mapping.get(id);\n if (resolved == null) {\n throw new Error('Could not resolve bundle with id ' + id);\n }\n return new URL(resolved.path, resolved.baseUrl).toString();\n}\nmodule.exports.register = register;\nmodule.exports.resolve = resolve;","require('./helpers/bundle-manifest').register(new __parcel__URL__(\"\").toString(),JSON.parse(\"[\\\"1LzKV\\\",\\\"index.585f762b.js\\\",\\\"kibx1\\\",\\\"browser.a9eca276.js\\\"]\"));"],"names":["$parcel$export","e","n","v","s","Object","defineProperty","get","set","enumerable","configurable","$parcel$global","globalThis","$parcel$modules","$parcel$inits","parcelRequire","id","exports","init","module","call","err","Error","code","register","parcelRegister","$18c11f3350a906ea$export$6503ec6e8aabbaf","$18c11f3350a906ea$export$f7ad0328861e2f03","$18c11f3350a906ea$var$mapping","Map","baseUrl","manifest","i","length","path","resolved","URL","toString","url","JSON","parse"],"version":3,"file":"index.runtime.ef08fcd2.js.map"} -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@cosmograph/cosmograph": "^1.3.1" 4 | }, 5 | "name": "wally", 6 | "version": "1.0.0", 7 | "devDependencies": { 8 | "cssnano": "^7.0.1", 9 | "parcel": "^2.11.0", 10 | "postcss": "^8.4.38", 11 | "prettier": "3.2.5", 12 | "rimraf": "^5.0.5", 13 | "tailwindcss": "^3.4.1", 14 | "typescript": "^5.3.3" 15 | }, 16 | "scripts": { 17 | "clean:output": "rimraf dist", 18 | "build": "npm run clean:output && parcel build src/index.html", 19 | "watch": "npm run clean:output && parcel watch src/index.html" 20 | }, 21 | "author": "Alex Useche", 22 | "license": "ISC", 23 | "description": "" 24 | } 25 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "io/fs" 7 | "log" 8 | "net/http" 9 | "strconv" 10 | ) 11 | 12 | //go:embed dist 13 | var public embed.FS 14 | 15 | var jsonFile []byte 16 | 17 | func setupHandlers() { 18 | http.HandleFunc("/wally.json", jsonHandler) 19 | 20 | publicFS, err := fs.Sub(public, "dist") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | http.Handle("/", http.FileServer(http.FS(publicFS))) 26 | } 27 | 28 | func jsonHandler(w http.ResponseWriter, r *http.Request) { 29 | w.Write(jsonFile) 30 | } 31 | 32 | func ServerCosmograph(file []byte, port int) { 33 | jsonFile = file 34 | 35 | setupHandlers() 36 | 37 | if port == 0 { 38 | port = 1984 39 | } 40 | portStr := strconv.Itoa(port) 41 | 42 | fmt.Printf("Wally server running on http://localhost:%s", portStr) 43 | 44 | err := http.ListenAndServe(fmt.Sprintf(":%s", portStr), nil) 45 | if err != nil { 46 | log.Fatal("Unable to start server with err: ", err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/src/cosmograph/config.ts: -------------------------------------------------------------------------------- 1 | export const BaseConfig = { 2 | backgroundColor: "#0f172a", 3 | nodeSize: 2.0, 4 | nodeLabelColor: "white", 5 | simulationRepulsion: 1.6, 6 | simulationLinkDistance: 10, 7 | nodeLabelClassName: "css-label--label", 8 | }; 9 | -------------------------------------------------------------------------------- /server/src/cosmograph/graph.ts: -------------------------------------------------------------------------------- 1 | import { Cosmograph, CosmographSearch } from "@cosmograph/cosmograph"; 2 | import { BaseConfig } from "./config"; 3 | import { Node, Link } from "./types"; 4 | 5 | export class WallyGraph { 6 | data: any; 7 | nodes: Node[] = []; 8 | links: Link[] = []; 9 | clickedNodes: string[] = []; 10 | clickedNodeId: string = ""; 11 | config: any; 12 | searchConfig: any; 13 | cosmograph: Cosmograph; 14 | cosmoSearch: CosmographSearch; 15 | 16 | constructor(data: any) { 17 | this.data = data; 18 | this.setConfig(); 19 | this.setNodes(); 20 | 21 | const cosmographContainer = document.getElementById("cosmograph")!; 22 | const searchContainer = document.getElementById("cosmosearch"); 23 | 24 | this.cosmograph = new Cosmograph( 25 | cosmographContainer as HTMLDivElement, 26 | ); 27 | this.cosmoSearch = new CosmographSearch( 28 | this.cosmograph, 29 | searchContainer as HTMLDivElement, 30 | ); 31 | } 32 | 33 | private setConfig() { 34 | this.config = BaseConfig; 35 | this.config.nodeColor = (n: any) => this.getClickedNodesColor(n); 36 | this.config.linkColor = (l: any) => this.getLinkColor(l); 37 | this.config.nodeLabelAccessor = (n: any) => this.getLabel(n); 38 | this.config.onClick = (node: any, i: any) => this.onNodeClick(node, i); 39 | 40 | this.searchConfig = { 41 | maxVisibleItems: 5, 42 | activeAccessorIndex: 0, 43 | events: { 44 | onSelect: (node: { id: any }) => { 45 | console.log("Selected Node: ", node.id); 46 | }, 47 | }, 48 | }; 49 | } 50 | 51 | private setNodes() { 52 | try { 53 | this.parseData(); 54 | } catch (error) { 55 | console.error("Error loading Wally data:", error); 56 | } 57 | } 58 | 59 | private parseData() { 60 | this.data.forEach((finding: any) => { 61 | finding.Paths.forEach((paths: string[]) => { 62 | let prev = ""; 63 | paths.forEach((path, i) => { 64 | if (i === 0) { 65 | prev = path; 66 | this.addNodeIfNotExist(path, "purple", ""); 67 | } else { 68 | if (i == paths.length - 1) { 69 | this.addNodeIfNotExist(path, "#984040", finding.MatchId); 70 | } else { 71 | this.addNodeIfNotExist(path, "#4287f5", ""); 72 | } 73 | this.addEdgeIfNotExist(prev, path); 74 | prev = path; 75 | } 76 | }); 77 | }); 78 | }); 79 | } 80 | 81 | private nodeExists(nodeId: string): boolean { 82 | return this.nodes.some((node) => node.id === nodeId); 83 | } 84 | 85 | private edgeExists(source: string, target: string): boolean { 86 | return this.links.some( 87 | (link) => link.source === source && link.target === target, 88 | ); 89 | } 90 | 91 | private addNodeIfNotExist(nodeId: string, color: string, findingId: string) { 92 | if (!this.nodeExists(nodeId)) { 93 | let label = this.extractFuncFromId(nodeId); 94 | label = label != null ? label : ""; 95 | this.nodes.push({ 96 | id: nodeId, 97 | label: label, 98 | color: color, 99 | green: "green", 100 | finding: findingId, 101 | }); 102 | } else { 103 | let node = this.nodes.find((node) => node.id === nodeId); 104 | if (node != null) { 105 | if (color == "#4287f5" && node.color == "purple") { 106 | node.color = "#FFCE85"; 107 | } 108 | 109 | if (node.color == "#4287f5" && color == "purple") { 110 | node.color = "#FFCE85"; 111 | } 112 | } 113 | } 114 | } 115 | 116 | private addEdgeIfNotExist(source: string, target: string) { 117 | if (!this.edgeExists(source, target)) { 118 | this.links.push({ source, target, color: "#8C8C8C" }); 119 | } 120 | } 121 | 122 | findAllPrecedingNodes(nodeId: string): any { 123 | let visited = new Set(); // To keep track of visited nodes 124 | let stack = [nodeId]; // Start with the target node 125 | 126 | while (stack.length > 0) { 127 | let current = stack.pop(); 128 | 129 | // Add the current node to the visited set 130 | visited.add(current); 131 | 132 | // Find all links where the current node is a target 133 | let incomingLinks = this.links.filter( 134 | (link: { target: string | undefined }) => link.target === current, 135 | ); 136 | incomingLinks.forEach((link: { source: string }) => { 137 | // Add the source node of each link to the stack 138 | if (!visited.has(link.source)) { 139 | stack.push(link.source); 140 | } 141 | }); 142 | } 143 | 144 | visited.delete(nodeId); // Remove the initial node from the result 145 | return Array.from(visited); // Convert the Set of visited nodes to an Array and return 146 | } 147 | 148 | findLinksByNodeId(nodeId: string): Link[] { 149 | return this.links.filter( 150 | (link: { source: string; target: string }) => 151 | link.source === nodeId || link.target === nodeId, 152 | ); 153 | } 154 | 155 | onNodeClick(node: { id: string; finding: string } | undefined, i: any) { 156 | if (node == undefined) { 157 | this.clickedNodes = []; 158 | this.clickedNodeId = ""; 159 | detailsOff(); 160 | 161 | this.cosmoSearch.clearInput(); 162 | this.cosmograph.unselectNodes(); 163 | } else { 164 | let conn = this.findAllPrecedingNodes(node.id); 165 | this.clickedNodes = conn; 166 | this.clickedNodes.push(node.id); 167 | this.clickedNodeId = node.id; 168 | 169 | if (node.finding != "") { 170 | let finding = this.getFinding(node.finding); 171 | setLeftSide(finding); 172 | } 173 | } 174 | 175 | this.config.nodeColor = (n: Node) => this.getClickedNodesColor(n); 176 | this.config.linkColor = (l: Link) => this.getLinkColor(l); 177 | this.cosmograph.setConfig(this.config); 178 | } 179 | 180 | getClickedNodesColor(node: Node) { 181 | const defaultColor = node.color; 182 | 183 | if (this.clickedNodes.includes(node.id)) { 184 | return node.color; 185 | } else { 186 | if (this.clickedNodes.length > 0) { 187 | return [0, 0, 0, 0]; 188 | } else { 189 | if (node.color == "purple" || node.color == "#984040") { 190 | return node.color; 191 | } 192 | return defaultColor; 193 | } 194 | } 195 | } 196 | 197 | getClickedNodeColor(node: Node) { 198 | const defaultColor = node.color; 199 | const clickedColor = "green"; 200 | 201 | if (this.clickedNodeId == node.id) { 202 | return clickedColor; 203 | } else { 204 | return defaultColor; 205 | } 206 | } 207 | 208 | setupGraph() { 209 | this.cosmograph.setConfig(BaseConfig); 210 | this.cosmoSearch.setConfig(this.searchConfig); 211 | this.cosmograph.setData(this.nodes, this.links); 212 | } 213 | 214 | extractFuncFromId(nodeId: string): string | null { 215 | const match = nodeId.match(/\[(.*?)\]/); 216 | return match ? match[1] : null; 217 | } 218 | 219 | getLinkColor(link: Link) { 220 | let nt = this.clickedNodes.find((n) => link.target === n); 221 | if (nt != undefined && nt != null) { 222 | return "green"; 223 | } 224 | if (this.clickedNodes.length > 0) { 225 | return [0, 0, 0, 0]; 226 | } 227 | return link.color; 228 | } 229 | 230 | getLabel(node: Node) { 231 | if (this.clickedNodes.includes(node.id)) { 232 | return node.id; 233 | } else { 234 | if (this.clickedNodes.length > 0) { 235 | return ""; 236 | } else { 237 | return node.label; 238 | } 239 | } 240 | } 241 | 242 | getFinding(findingId: string): any { 243 | let res: any; 244 | this.data.forEach((finding: any) => { 245 | if (findingId == finding.MatchId) { 246 | res = finding; 247 | } 248 | }); 249 | return res; 250 | } 251 | } 252 | 253 | // Containers in UI 254 | const details = document.getElementById("details"); 255 | 256 | export function detailsOn() { 257 | if (details != null && details.classList.contains("invisible")) { 258 | details.classList.remove("invisible"); 259 | } 260 | } 261 | 262 | export function detailsOff() { 263 | if (details != null && !details.classList.contains("invisible")) { 264 | details.classList.add("invisible"); 265 | } 266 | } 267 | 268 | export function setLeftSide(finding: any) { 269 | detailsOn(); 270 | document.getElementById("pkg")!.textContent = finding.Indicator.Package; 271 | document.getElementById("func")!.textContent = finding.Indicator.Function; 272 | document.getElementById("params")!.textContent = JSON.stringify( 273 | finding.Indicator.Params, 274 | ); 275 | document.getElementById("enclosedBy")!.textContent = finding.EnclosedBy; 276 | document.getElementById("pos")!.textContent = finding.Pos; 277 | document.getElementById("pathNum")!.textContent = finding.Paths.length; 278 | } 279 | -------------------------------------------------------------------------------- /server/src/cosmograph/types.ts: -------------------------------------------------------------------------------- 1 | export type Node = { 2 | id: string; 3 | color: string; 4 | green: string; 5 | label: string; 6 | finding: string; 7 | x?: number; 8 | y?: number; 9 | }; 10 | 11 | export type Link = { 12 | source: string; 13 | target: string; 14 | color: string; 15 | }; 16 | -------------------------------------------------------------------------------- /server/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Wally 9 | 10 | 11 |
12 | 13 | 17 |
18 | 21 | 29 |
30 | 31 |
32 | 33 |

36 | Wally 37 |

38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /server/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Node, Link } from "./cosmograph/types"; 2 | import { WallyGraph } from "./cosmograph/graph"; 3 | 4 | // Function to fetch and parse wally data 5 | async function loadWallyData() { 6 | const fileName = "wally.json"; 7 | try { 8 | const fileUrl = `/${fileName}`; 9 | 10 | // Fetch wally file hosted by Go 11 | const response = await fetch(fileUrl); 12 | if (!response.ok) { 13 | throw new Error(`Failed to fetch ${fileUrl}: ${response.statusText}`); 14 | } 15 | 16 | const jsonData = await response.json(); 17 | const wallyGraph = new WallyGraph(jsonData); 18 | wallyGraph.setupGraph(); 19 | } catch (error) { 20 | console.error("Error loading Wally data:", error); 21 | } 22 | } 23 | 24 | loadWallyData(); 25 | -------------------------------------------------------------------------------- /server/src/styles/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* .invisible { 6 | visibility: hidden; 7 | } */ 8 | -------------------------------------------------------------------------------- /server/src/styles/output.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com 3 | */ 4 | 5 | /* 6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 8 | */ 9 | 10 | *, 11 | ::before, 12 | ::after { 13 | box-sizing: border-box; 14 | /* 1 */ 15 | border-width: 0; 16 | /* 2 */ 17 | border-style: solid; 18 | /* 2 */ 19 | border-color: #e5e7eb; 20 | /* 2 */ 21 | } 22 | 23 | ::before, 24 | ::after { 25 | --tw-content: ""; 26 | } 27 | 28 | /* 29 | 1. Use a consistent sensible line-height in all browsers. 30 | 2. Prevent adjustments of font size after orientation changes in iOS. 31 | 3. Use a more readable tab size. 32 | 4. Use the user's configured `sans` font-family by default. 33 | 5. Use the user's configured `sans` font-feature-settings by default. 34 | 6. Use the user's configured `sans` font-variation-settings by default. 35 | 7. Disable tap highlights on iOS 36 | */ 37 | 38 | html, 39 | :host { 40 | line-height: 1.5; 41 | /* 1 */ 42 | -webkit-text-size-adjust: 100%; 43 | /* 2 */ 44 | -moz-tab-size: 4; 45 | /* 3 */ 46 | -o-tab-size: 4; 47 | tab-size: 4; 48 | /* 3 */ 49 | font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", 50 | "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 51 | /* 4 */ 52 | font-feature-settings: normal; 53 | /* 5 */ 54 | font-variation-settings: normal; 55 | /* 6 */ 56 | -webkit-tap-highlight-color: transparent; 57 | /* 7 */ 58 | } 59 | 60 | /* 61 | 1. Remove the margin in all browsers. 62 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 63 | */ 64 | 65 | body { 66 | margin: 0; 67 | /* 1 */ 68 | line-height: inherit; 69 | /* 2 */ 70 | } 71 | 72 | /* 73 | 1. Add the correct height in Firefox. 74 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 75 | 3. Ensure horizontal rules are visible by default. 76 | */ 77 | 78 | hr { 79 | height: 0; 80 | /* 1 */ 81 | color: inherit; 82 | /* 2 */ 83 | border-top-width: 1px; 84 | /* 3 */ 85 | } 86 | 87 | /* 88 | Add the correct text decoration in Chrome, Edge, and Safari. 89 | */ 90 | 91 | abbr:where([title]) { 92 | -webkit-text-decoration: underline dotted; 93 | text-decoration: underline dotted; 94 | } 95 | 96 | /* 97 | Remove the default font size and weight for headings. 98 | */ 99 | 100 | h1, 101 | h2, 102 | h3, 103 | h4, 104 | h5, 105 | h6 { 106 | font-size: inherit; 107 | font-weight: inherit; 108 | } 109 | 110 | /* 111 | Reset links to optimize for opt-in styling instead of opt-out. 112 | */ 113 | 114 | a { 115 | color: inherit; 116 | text-decoration: inherit; 117 | } 118 | 119 | /* 120 | Add the correct font weight in Edge and Safari. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: bolder; 126 | } 127 | 128 | /* 129 | 1. Use the user's configured `mono` font-family by default. 130 | 2. Use the user's configured `mono` font-feature-settings by default. 131 | 3. Use the user's configured `mono` font-variation-settings by default. 132 | 4. Correct the odd `em` font sizing in all browsers. 133 | */ 134 | 135 | code, 136 | kbd, 137 | samp, 138 | pre { 139 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 140 | "Liberation Mono", "Courier New", monospace; 141 | /* 1 */ 142 | font-feature-settings: normal; 143 | /* 2 */ 144 | font-variation-settings: normal; 145 | /* 3 */ 146 | font-size: 1em; 147 | /* 4 */ 148 | } 149 | 150 | /* 151 | Add the correct font size in all browsers. 152 | */ 153 | 154 | small { 155 | font-size: 80%; 156 | } 157 | 158 | /* 159 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 160 | */ 161 | 162 | sub, 163 | sup { 164 | font-size: 75%; 165 | line-height: 0; 166 | position: relative; 167 | vertical-align: baseline; 168 | } 169 | 170 | sub { 171 | bottom: -0.25em; 172 | } 173 | 174 | sup { 175 | top: -0.5em; 176 | } 177 | 178 | /* 179 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 180 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 181 | 3. Remove gaps between table borders by default. 182 | */ 183 | 184 | table { 185 | text-indent: 0; 186 | /* 1 */ 187 | border-color: inherit; 188 | /* 2 */ 189 | border-collapse: collapse; 190 | /* 3 */ 191 | } 192 | 193 | /* 194 | 1. Change the font styles in all browsers. 195 | 2. Remove the margin in Firefox and Safari. 196 | 3. Remove default padding in all browsers. 197 | */ 198 | 199 | button, 200 | input, 201 | optgroup, 202 | select, 203 | textarea { 204 | font-family: inherit; 205 | /* 1 */ 206 | font-feature-settings: inherit; 207 | /* 1 */ 208 | font-variation-settings: inherit; 209 | /* 1 */ 210 | font-size: 100%; 211 | /* 1 */ 212 | font-weight: inherit; 213 | /* 1 */ 214 | line-height: inherit; 215 | /* 1 */ 216 | color: inherit; 217 | /* 1 */ 218 | margin: 0; 219 | /* 2 */ 220 | padding: 0; 221 | /* 3 */ 222 | } 223 | 224 | /* 225 | Remove the inheritance of text transform in Edge and Firefox. 226 | */ 227 | 228 | button, 229 | select { 230 | text-transform: none; 231 | } 232 | 233 | /* 234 | 1. Correct the inability to style clickable types in iOS and Safari. 235 | 2. Remove default button styles. 236 | */ 237 | 238 | button, 239 | [type="button"], 240 | [type="reset"], 241 | [type="submit"] { 242 | -webkit-appearance: button; 243 | /* 1 */ 244 | background-color: transparent; 245 | /* 2 */ 246 | background-image: none; 247 | /* 2 */ 248 | } 249 | 250 | /* 251 | Use the modern Firefox focus style for all focusable elements. 252 | */ 253 | 254 | :-moz-focusring { 255 | outline: auto; 256 | } 257 | 258 | /* 259 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 260 | */ 261 | 262 | :-moz-ui-invalid { 263 | box-shadow: none; 264 | } 265 | 266 | /* 267 | Add the correct vertical alignment in Chrome and Firefox. 268 | */ 269 | 270 | progress { 271 | vertical-align: baseline; 272 | } 273 | 274 | /* 275 | Correct the cursor style of increment and decrement buttons in Safari. 276 | */ 277 | 278 | ::-webkit-inner-spin-button, 279 | ::-webkit-outer-spin-button { 280 | height: auto; 281 | } 282 | 283 | /* 284 | 1. Correct the odd appearance in Chrome and Safari. 285 | 2. Correct the outline style in Safari. 286 | */ 287 | 288 | [type="search"] { 289 | -webkit-appearance: textfield; 290 | /* 1 */ 291 | outline-offset: -2px; 292 | /* 2 */ 293 | } 294 | 295 | /* 296 | Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | ::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /* 304 | 1. Correct the inability to style clickable types in iOS and Safari. 305 | 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; 310 | /* 1 */ 311 | font: inherit; 312 | /* 2 */ 313 | } 314 | 315 | /* 316 | Add the correct display in Chrome and Safari. 317 | */ 318 | 319 | summary { 320 | display: list-item; 321 | } 322 | 323 | /* 324 | Removes the default spacing and border for appropriate elements. 325 | */ 326 | 327 | blockquote, 328 | dl, 329 | dd, 330 | h1, 331 | h2, 332 | h3, 333 | h4, 334 | h5, 335 | h6, 336 | hr, 337 | figure, 338 | p, 339 | pre { 340 | margin: 0; 341 | } 342 | 343 | fieldset { 344 | margin: 0; 345 | padding: 0; 346 | } 347 | 348 | legend { 349 | padding: 0; 350 | } 351 | 352 | ol, 353 | ul, 354 | menu { 355 | list-style: none; 356 | margin: 0; 357 | padding: 0; 358 | } 359 | 360 | /* 361 | Reset default styling for dialogs. 362 | */ 363 | 364 | dialog { 365 | padding: 0; 366 | } 367 | 368 | /* 369 | Prevent resizing textareas horizontally by default. 370 | */ 371 | 372 | textarea { 373 | resize: vertical; 374 | } 375 | 376 | /* 377 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 378 | 2. Set the default placeholder color to the user's configured gray 400 color. 379 | */ 380 | 381 | input::-moz-placeholder, 382 | textarea::-moz-placeholder { 383 | opacity: 1; 384 | /* 1 */ 385 | color: #9ca3af; 386 | /* 2 */ 387 | } 388 | 389 | input::placeholder, 390 | textarea::placeholder { 391 | opacity: 1; 392 | /* 1 */ 393 | color: #9ca3af; 394 | /* 2 */ 395 | } 396 | 397 | /* 398 | Set the default cursor for buttons. 399 | */ 400 | 401 | button, 402 | [role="button"] { 403 | cursor: pointer; 404 | } 405 | 406 | /* 407 | Make sure disabled buttons don't get the pointer cursor. 408 | */ 409 | 410 | :disabled { 411 | cursor: default; 412 | } 413 | 414 | /* 415 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 416 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 417 | This can trigger a poorly considered lint error in some tools but is included by design. 418 | */ 419 | 420 | img, 421 | svg, 422 | video, 423 | canvas, 424 | audio, 425 | iframe, 426 | embed, 427 | object { 428 | display: block; 429 | /* 1 */ 430 | vertical-align: middle; 431 | /* 2 */ 432 | } 433 | 434 | /* 435 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 436 | */ 437 | 438 | img, 439 | video { 440 | max-width: 100%; 441 | height: auto; 442 | } 443 | 444 | /* Make elements with the HTML hidden attribute stay hidden by default */ 445 | 446 | [hidden] { 447 | display: none; 448 | } 449 | 450 | *, 451 | ::before, 452 | ::after { 453 | --tw-border-spacing-x: 0; 454 | --tw-border-spacing-y: 0; 455 | --tw-translate-x: 0; 456 | --tw-translate-y: 0; 457 | --tw-rotate: 0; 458 | --tw-skew-x: 0; 459 | --tw-skew-y: 0; 460 | --tw-scale-x: 1; 461 | --tw-scale-y: 1; 462 | --tw-pan-x: ; 463 | --tw-pan-y: ; 464 | --tw-pinch-zoom: ; 465 | --tw-scroll-snap-strictness: proximity; 466 | --tw-gradient-from-position: ; 467 | --tw-gradient-via-position: ; 468 | --tw-gradient-to-position: ; 469 | --tw-ordinal: ; 470 | --tw-slashed-zero: ; 471 | --tw-numeric-figure: ; 472 | --tw-numeric-spacing: ; 473 | --tw-numeric-fraction: ; 474 | --tw-ring-inset: ; 475 | --tw-ring-offset-width: 0px; 476 | --tw-ring-offset-color: #fff; 477 | --tw-ring-color: rgb(59 130 246 / 0.5); 478 | --tw-ring-offset-shadow: 0 0 #0000; 479 | --tw-ring-shadow: 0 0 #0000; 480 | --tw-shadow: 0 0 #0000; 481 | --tw-shadow-colored: 0 0 #0000; 482 | --tw-blur: ; 483 | --tw-brightness: ; 484 | --tw-contrast: ; 485 | --tw-grayscale: ; 486 | --tw-hue-rotate: ; 487 | --tw-invert: ; 488 | --tw-saturate: ; 489 | --tw-sepia: ; 490 | --tw-drop-shadow: ; 491 | --tw-backdrop-blur: ; 492 | --tw-backdrop-brightness: ; 493 | --tw-backdrop-contrast: ; 494 | --tw-backdrop-grayscale: ; 495 | --tw-backdrop-hue-rotate: ; 496 | --tw-backdrop-invert: ; 497 | --tw-backdrop-opacity: ; 498 | --tw-backdrop-saturate: ; 499 | --tw-backdrop-sepia: ; 500 | } 501 | 502 | ::backdrop { 503 | --tw-border-spacing-x: 0; 504 | --tw-border-spacing-y: 0; 505 | --tw-translate-x: 0; 506 | --tw-translate-y: 0; 507 | --tw-rotate: 0; 508 | --tw-skew-x: 0; 509 | --tw-skew-y: 0; 510 | --tw-scale-x: 1; 511 | --tw-scale-y: 1; 512 | --tw-pan-x: ; 513 | --tw-pan-y: ; 514 | --tw-pinch-zoom: ; 515 | --tw-scroll-snap-strictness: proximity; 516 | --tw-gradient-from-position: ; 517 | --tw-gradient-via-position: ; 518 | --tw-gradient-to-position: ; 519 | --tw-ordinal: ; 520 | --tw-slashed-zero: ; 521 | --tw-numeric-figure: ; 522 | --tw-numeric-spacing: ; 523 | --tw-numeric-fraction: ; 524 | --tw-ring-inset: ; 525 | --tw-ring-offset-width: 0px; 526 | --tw-ring-offset-color: #fff; 527 | --tw-ring-color: rgb(59 130 246 / 0.5); 528 | --tw-ring-offset-shadow: 0 0 #0000; 529 | --tw-ring-shadow: 0 0 #0000; 530 | --tw-shadow: 0 0 #0000; 531 | --tw-shadow-colored: 0 0 #0000; 532 | --tw-blur: ; 533 | --tw-brightness: ; 534 | --tw-contrast: ; 535 | --tw-grayscale: ; 536 | --tw-hue-rotate: ; 537 | --tw-invert: ; 538 | --tw-saturate: ; 539 | --tw-sepia: ; 540 | --tw-drop-shadow: ; 541 | --tw-backdrop-blur: ; 542 | --tw-backdrop-brightness: ; 543 | --tw-backdrop-contrast: ; 544 | --tw-backdrop-grayscale: ; 545 | --tw-backdrop-hue-rotate: ; 546 | --tw-backdrop-invert: ; 547 | --tw-backdrop-opacity: ; 548 | --tw-backdrop-saturate: ; 549 | --tw-backdrop-sepia: ; 550 | } 551 | 552 | .invisible { 553 | visibility: hidden; 554 | } 555 | 556 | .absolute { 557 | position: absolute; 558 | } 559 | 560 | .relative { 561 | position: relative; 562 | } 563 | 564 | .right-0 { 565 | right: 0px; 566 | } 567 | 568 | .top-0 { 569 | top: 0px; 570 | } 571 | 572 | .z-0 { 573 | z-index: 0; 574 | } 575 | 576 | .z-10 { 577 | z-index: 10; 578 | } 579 | 580 | .mb-4 { 581 | margin-bottom: 1rem; 582 | } 583 | 584 | .block { 585 | display: block; 586 | } 587 | 588 | .flex { 589 | display: flex; 590 | } 591 | 592 | .h-screen { 593 | height: 100vh; 594 | } 595 | 596 | .w-64 { 597 | width: 16rem; 598 | } 599 | 600 | .w-full { 601 | width: 100%; 602 | } 603 | 604 | .flex-1 { 605 | flex: 1 1 0%; 606 | } 607 | 608 | .flex-none { 609 | flex: none; 610 | } 611 | 612 | .flex-row { 613 | flex-direction: row; 614 | } 615 | 616 | .space-x-1 > :not([hidden]) ~ :not([hidden]) { 617 | --tw-space-x-reverse: 0; 618 | margin-right: calc(0.25rem * var(--tw-space-x-reverse)); 619 | margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse))); 620 | } 621 | 622 | .space-y-2 > :not([hidden]) ~ :not([hidden]) { 623 | --tw-space-y-reverse: 0; 624 | margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); 625 | margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); 626 | } 627 | 628 | .break-words { 629 | overflow-wrap: break-word; 630 | } 631 | 632 | .rounded { 633 | border-radius: 0.25rem; 634 | } 635 | 636 | .bg-gray-100 { 637 | --tw-bg-opacity: 1; 638 | background-color: rgb(243 244 246 / var(--tw-bg-opacity)); 639 | } 640 | 641 | .bg-gray-800 { 642 | --tw-bg-opacity: 1; 643 | background-color: rgb(31 41 55 / var(--tw-bg-opacity)); 644 | } 645 | 646 | .bg-gray-900 { 647 | --tw-bg-opacity: 1; 648 | background-color: rgb(17 24 39 / var(--tw-bg-opacity)); 649 | } 650 | 651 | .bg-transparent { 652 | background-color: transparent; 653 | } 654 | 655 | .p-4 { 656 | padding: 1rem; 657 | } 658 | 659 | .px-4 { 660 | padding-left: 1rem; 661 | padding-right: 1rem; 662 | } 663 | 664 | .py-2 { 665 | padding-top: 0.5rem; 666 | padding-bottom: 0.5rem; 667 | } 668 | 669 | .text-center { 670 | text-align: center; 671 | } 672 | 673 | .text-right { 674 | text-align: right; 675 | } 676 | 677 | .text-2xl { 678 | font-size: 1.5rem; 679 | line-height: 2rem; 680 | } 681 | 682 | .text-lg { 683 | font-size: 1.125rem; 684 | line-height: 1.75rem; 685 | } 686 | 687 | .text-sm { 688 | font-size: 0.875rem; 689 | line-height: 1.25rem; 690 | } 691 | 692 | .text-xs { 693 | font-size: 0.75rem; 694 | line-height: 1rem; 695 | } 696 | 697 | .font-bold { 698 | font-weight: 700; 699 | } 700 | 701 | .font-semibold { 702 | font-weight: 600; 703 | } 704 | 705 | .text-white { 706 | --tw-text-opacity: 1; 707 | color: rgb(255 255 255 / var(--tw-text-opacity)); 708 | } 709 | 710 | /* .invisible { 711 | visibility: hidden; 712 | } */ 713 | 714 | .hover\:bg-gray-700:hover { 715 | --tw-bg-opacity: 1; 716 | background-color: rgb(55 65 81 / var(--tw-bg-opacity)); 717 | } 718 | 719 | .focus\:bg-gray-700:focus { 720 | --tw-bg-opacity: 1; 721 | background-color: rgb(55 65 81 / var(--tw-bg-opacity)); 722 | } 723 | 724 | .focus\:outline-none:focus { 725 | outline: 2px solid transparent; 726 | outline-offset: 2px; 727 | } 728 | -------------------------------------------------------------------------------- /server/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{html,js}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | "resolveJsonModule": true /* Enable importing .json files. */, 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 83 | 84 | /* Type Checking */ 85 | "strict": true /* Enable all strict type-checking options. */, 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /wallylib/callmapper/callmapper.go: -------------------------------------------------------------------------------- 1 | package callmapper 2 | 3 | import ( 4 | "container/list" 5 | "fmt" 6 | "github.com/hex0punk/wally/match" 7 | "github.com/hex0punk/wally/wallylib" 8 | "github.com/hex0punk/wally/wallynode" 9 | "go/token" 10 | "golang.org/x/tools/go/callgraph" 11 | "golang.org/x/tools/go/ssa" 12 | "strings" 13 | ) 14 | 15 | type SearchAlgorithm int 16 | 17 | const ( 18 | Bfs SearchAlgorithm = iota 19 | Dfs 20 | ) 21 | 22 | type CallMapper struct { 23 | Options Options 24 | Match *match.RouteMatch 25 | Stop bool 26 | CallgraphNodes map[*ssa.Function]*callgraph.Node 27 | NodeFactory *wallynode.WallyNodeFactory 28 | } 29 | 30 | var SearchAlgs = map[string]SearchAlgorithm{ 31 | "bfs": Bfs, 32 | "dfs": Dfs, 33 | } 34 | 35 | // TODO: this should be path of the callpath structs in match pkg 36 | type BFSNode struct { 37 | Node *callgraph.Node 38 | Path []wallynode.WallyNode 39 | } 40 | 41 | type LimiterMode int 42 | 43 | // None = allows analysis to run pass main 44 | // Normal = filters up to the main function if possible unless. It also filters up to main pkg *unless* the last function before going outside of main is a closure 45 | // Strict = Does not allow going past the main package 46 | const ( 47 | None LimiterMode = iota 48 | Normal 49 | High 50 | Strict 51 | VeryStrict 52 | ) 53 | 54 | var LimiterModes = map[string]LimiterMode{ 55 | "none": None, 56 | "normal": Normal, 57 | "high": High, 58 | "strict": Strict, 59 | "very-strict": VeryStrict, 60 | } 61 | 62 | type Options struct { 63 | Filter string 64 | MaxFuncs int 65 | MaxPaths int 66 | PrintNodes bool 67 | SearchAlg SearchAlgorithm 68 | Limiter LimiterMode 69 | SkipClosures bool 70 | ModuleOnly bool 71 | Simplify bool 72 | } 73 | 74 | func NewCallMapper(match *match.RouteMatch, nodes map[*ssa.Function]*callgraph.Node, options Options) *CallMapper { 75 | // Rather than adding another state to check for, this is easier 76 | if options.ModuleOnly && match.Module != "" { 77 | options.Filter = match.Module 78 | } 79 | nodeFactory := wallynode.NewWallyNodeFactory(nodes) 80 | return &CallMapper{ 81 | Options: options, 82 | Match: match, 83 | CallgraphNodes: nodes, 84 | NodeFactory: nodeFactory, 85 | } 86 | } 87 | 88 | func (cm *CallMapper) initPath(s *callgraph.Node) []wallynode.WallyNode { 89 | encPkg := cm.Match.SSA.EnclosedByFunc.Pkg 90 | encBasePos := wallylib.GetFormattedPos(encPkg, cm.Match.SSA.EnclosedByFunc.Pos()) 91 | rec := wallynode.IsRecoverable(s, cm.CallgraphNodes) 92 | encStr := wallynode.GetNodeString(encBasePos, s, rec) 93 | 94 | //if cm.Options.Simplify { 95 | // cm.Match.SSA.TargetPos = encStr 96 | // return []wallynode.WallyNode{} 97 | //} 98 | 99 | // TODO: No real reason for this to be here 100 | siteStr := "" 101 | if cm.Match.SSA.SSAInstruction == nil { 102 | encStr = cm.Match.Pos.String() 103 | } else { 104 | sitePkg := cm.Match.SSA.SSAInstruction.Parent().Pkg 105 | 106 | // cm.Options.Simplify should be false if here 107 | siteBasePos := wallylib.GetFormattedPos(sitePkg, cm.Match.SSA.SSAInstruction.Pos()) 108 | if cm.Match.SSA.SSAFunc == nil { 109 | siteStr = fmt.Sprintf("%s.[%s] %s", sitePkg.Pkg.Name(), cm.Match.Indicator.Function, siteBasePos) 110 | } else { 111 | targetFuncNode := cm.CallgraphNodes[cm.Match.SSA.SSAFunc] 112 | isRec := wallynode.IsRecoverable(targetFuncNode, cm.CallgraphNodes) 113 | siteStr = wallynode.GetNodeString(siteBasePos, targetFuncNode, isRec) 114 | } 115 | cm.Match.SSA.TargetPos = siteStr 116 | } 117 | 118 | if cm.Options.Simplify { 119 | return []wallynode.WallyNode{} 120 | } 121 | 122 | initialPath := []wallynode.WallyNode{ 123 | {NodeString: encStr, Caller: s}, 124 | } 125 | return initialPath 126 | } 127 | 128 | func (cm *CallMapper) AllPathsBFS(s *callgraph.Node) *match.CallPaths { 129 | initialPath := cm.initPath(s) 130 | callPaths := &match.CallPaths{} 131 | cm.BFS(s, initialPath, callPaths) 132 | return callPaths 133 | } 134 | 135 | func (cm *CallMapper) AllPathsDFS(s *callgraph.Node) *match.CallPaths { 136 | visited := make(map[int]bool) 137 | initialPath := cm.initPath(s) 138 | callPaths := &match.CallPaths{} 139 | callPaths.Paths = []*match.CallPath{} 140 | cm.DFS(s, visited, initialPath, callPaths, nil) 141 | return callPaths 142 | } 143 | 144 | func (cm *CallMapper) DFS(destination *callgraph.Node, visited map[int]bool, path []wallynode.WallyNode, paths *match.CallPaths, site ssa.CallInstruction) { 145 | if cm.Options.Limiter > None && destination.Func.Pos() == token.NoPos { 146 | return 147 | } 148 | newPath := cm.appendNodeToPath(destination, path, site) 149 | 150 | if cm.Options.Limiter > None && isMainFunc(destination) { 151 | paths.InsertPaths(newPath, false, false, cm.Options.Simplify) 152 | cm.Stop = false 153 | return 154 | } 155 | 156 | mustStop := cm.Options.MaxFuncs > 0 && len(newPath) >= cm.Options.MaxFuncs 157 | if len(destination.In) == 0 || mustStop || cm.Stop { 158 | paths.InsertPaths(newPath, mustStop, cm.Stop, cm.Options.Simplify) 159 | cm.Stop = false 160 | return 161 | } 162 | 163 | // Avoids recursion within a single callpath 164 | if visited[destination.ID] { 165 | paths.InsertPaths(newPath, false, false, cm.Options.Simplify) 166 | return 167 | } 168 | visited[destination.ID] = true 169 | 170 | defer delete(visited, destination.ID) 171 | 172 | cm.Stop = false 173 | allOutsideModule := true 174 | allOutsideMainPkg := true 175 | 176 | fnT := destination 177 | if cm.Options.Limiter >= Strict || cm.Options.SkipClosures { 178 | fnT, newPath = cm.handleClosure(destination, newPath) 179 | } 180 | for _, e := range fnT.In { 181 | if e.Caller.Func.Package() == nil { 182 | continue 183 | } 184 | if paths.Paths != nil && cm.Options.MaxPaths > 0 && len(paths.Paths) >= cm.Options.MaxPaths { 185 | cm.Match.SSA.PathLimited = true 186 | continue 187 | } 188 | if visited[e.Caller.ID] { 189 | continue 190 | } 191 | 192 | if !shouldSkipNode(e, fnT, cm.Options) { 193 | if mainPkgLimited(fnT, e, cm.Options) { 194 | continue 195 | } 196 | allOutsideMainPkg = false 197 | allOutsideModule = false 198 | cm.DFS(e.Caller, visited, newPath, paths, e.Site) 199 | } 200 | } 201 | if allOutsideModule { 202 | // TODO: This is a quick and dirty solution to marking a path as going outside the module 203 | // This should be handled diffirently and not abuse CallMapper struct 204 | cm.Stop = true 205 | } 206 | if allOutsideMainPkg { 207 | paths.InsertPaths(newPath, mustStop, cm.Stop, cm.Options.Simplify) 208 | cm.Stop = false 209 | return 210 | } 211 | } 212 | 213 | func (cm *CallMapper) BFS(start *callgraph.Node, initialPath []wallynode.WallyNode, paths *match.CallPaths) { 214 | queue := list.New() 215 | queue.PushBack(BFSNode{Node: start, Path: initialPath}) 216 | 217 | pathLimited := false 218 | for queue.Len() > 0 { 219 | //printQueue(queue) 220 | // we process the first node 221 | bfsNodeElm := queue.Front() 222 | // We remove last elm, so we can put it in the front after updating it with new paths 223 | queue.Remove(bfsNodeElm) 224 | 225 | current := bfsNodeElm.Value.(BFSNode) 226 | currentNode := current.Node 227 | currentPath := current.Path 228 | //printQueue(queue) 229 | 230 | if cm.Options.Limiter > None && currentNode.Func.Pos() == token.NoPos { 231 | paths.InsertPaths(currentPath, false, false, cm.Options.Simplify) 232 | continue 233 | } 234 | 235 | if cm.Options.Limiter > None && isMainFunc(currentNode) { 236 | paths.InsertPaths(currentPath, false, false, cm.Options.Simplify) 237 | continue 238 | } 239 | 240 | // Are we out of nodes for this currentNode, or have we reached the limit of funcs in a path? 241 | if limitFuncsReached(currentPath, cm.Options) { 242 | paths.InsertPaths(currentPath, true, false, cm.Options.Simplify) 243 | continue 244 | } 245 | 246 | var newPath []wallynode.WallyNode 247 | iterNode := currentNode 248 | if cm.Options.Limiter >= Strict || cm.Options.SkipClosures { 249 | iterNode, newPath = cm.handleClosure(currentNode, currentPath) 250 | } else { 251 | newPath = cm.appendNodeToPath(currentNode, currentPath, nil) 252 | } 253 | 254 | allOutsideFilter, allOutsideMainPkg, allAlreadyInPath := true, true, true 255 | allMismatchSite := true 256 | for _, e := range iterNode.In { 257 | if e.Caller.Func.Package() == nil { 258 | continue 259 | } 260 | if e.Site == nil { 261 | continue 262 | } 263 | // Do we care about this node, or is it in the path already (if it calls itself)? 264 | if cm.callerInPath(e, newPath) { 265 | continue 266 | } 267 | if cm.Options.Limiter >= VeryStrict { 268 | // make sure that site matches the function of the current node 269 | if !wallylib.SiteMatchesFunc(e.Site, iterNode.Func) { 270 | allMismatchSite = false 271 | allAlreadyInPath = false 272 | continue 273 | } 274 | } 275 | if cm.Options.Filter == "" || passesFilter(e.Caller, cm.Options.Filter) { 276 | if mainPkgLimited(iterNode, e, cm.Options) { 277 | allAlreadyInPath = false 278 | continue 279 | } 280 | 281 | allMismatchSite = false 282 | allOutsideMainPkg = false 283 | allOutsideFilter = false 284 | allAlreadyInPath = false 285 | // We care. So let's create a copy of the path. On first iteration this has only our two intial nodes 286 | newPathCopy := make([]wallynode.WallyNode, len(newPath)) 287 | copy(newPathCopy, newPath) 288 | 289 | // We want to process the new node we added to the path. 290 | newPathWithCaller := cm.appendNodeToPath(e.Caller, newPathCopy, e.Site) 291 | queue.PushBack(BFSNode{Node: e.Caller, Path: newPathWithCaller}) 292 | //printQueue(queue) 293 | // Have we reached the max paths set by the user 294 | if cm.Options.MaxPaths > 0 && queue.Len()+len(paths.Paths) >= cm.Options.MaxPaths { 295 | pathLimited = true 296 | break 297 | } 298 | } 299 | } 300 | if allOutsideMainPkg && !allAlreadyInPath { 301 | paths.InsertPaths(newPath, false, false, cm.Options.Simplify) 302 | continue 303 | } 304 | if cm.Options.Filter != "" && allOutsideFilter { 305 | paths.InsertPaths(newPath, false, true, cm.Options.Simplify) 306 | continue 307 | } 308 | if allMismatchSite { 309 | paths.InsertPaths(currentPath, false, false, cm.Options.Simplify) 310 | continue 311 | } 312 | if allAlreadyInPath { 313 | paths.InsertPaths(newPath, false, false, cm.Options.Simplify) 314 | } 315 | } 316 | 317 | // Insert whataver is left by now 318 | for e := queue.Front(); e != nil; e = e.Next() { 319 | bfsNode := e.Value.(BFSNode) 320 | paths.InsertPaths(bfsNode.Path, false, false, cm.Options.Simplify) 321 | cm.Match.SSA.PathLimited = pathLimited 322 | } 323 | } 324 | 325 | func limitFuncsReached(path []wallynode.WallyNode, options Options) bool { 326 | return options.MaxFuncs > 0 && len(path) >= options.MaxFuncs 327 | } 328 | 329 | func isMainFunc(node *callgraph.Node) bool { 330 | return node.Func.Name() == "main" || strings.HasPrefix(node.Func.Name(), "main$") 331 | } 332 | 333 | // Used to help wrangle some of the unrealistic resutls from cha.Callgraph 334 | func mainPkgLimited(currentNode *callgraph.Node, e *callgraph.Edge, options Options) bool { 335 | if options.Limiter == None { 336 | return false 337 | } 338 | 339 | // This occurs if we are at init 340 | if currentNode.Func.Pos() == token.NoPos { 341 | return true 342 | } 343 | 344 | currentPkg := currentNode.Func.Package().Pkg 345 | callerPkg := e.Caller.Func.Package().Pkg 346 | 347 | if currentPkg.Name() != "main" { 348 | return false 349 | } 350 | 351 | isDifferentMainPkg := callerPkg.Name() == "main" && currentPkg.Path() != callerPkg.Path() 352 | isNonMainPkg := callerPkg.Name() != "main" && currentPkg.Path() != callerPkg.Path() 353 | isNonMainCallerOrClosure := isNonMainPkg && !wallylib.IsClosure(currentNode.Func) 354 | 355 | if options.Limiter == Normal { 356 | return isDifferentMainPkg || isNonMainCallerOrClosure 357 | } 358 | 359 | if options.Limiter >= High { 360 | return isDifferentMainPkg || isNonMainPkg 361 | } 362 | return false 363 | } 364 | 365 | func shouldSkipNode(e *callgraph.Edge, destination *callgraph.Node, options Options) bool { 366 | if options.Limiter >= VeryStrict { 367 | // make sure that site matches the function of the current node 368 | if !wallylib.SiteMatchesFunc(e.Site, destination.Func) { 369 | return true 370 | } 371 | } 372 | if options.Filter != "" && e.Caller != nil && !passesFilter(e.Caller, options.Filter) { 373 | return true 374 | } 375 | return false 376 | } 377 | 378 | func passesFilter(node *callgraph.Node, filter string) bool { 379 | if node.Func != nil && node.Func.Pkg != nil { 380 | return strings.HasPrefix(node.Func.Pkg.Pkg.Path(), filter) || node.Func.Pkg.Pkg.Path() == "main" 381 | } 382 | return false 383 | } 384 | 385 | func (cm *CallMapper) callerInPath(e *callgraph.Edge, paths []wallynode.WallyNode) bool { 386 | for _, p := range paths { 387 | if e.Caller.ID == p.Caller.ID { 388 | return true 389 | } 390 | } 391 | return false 392 | } 393 | 394 | func (cm *CallMapper) appendNodeToPath(s *callgraph.Node, path []wallynode.WallyNode, site ssa.CallInstruction) []wallynode.WallyNode { 395 | if site == nil && !cm.Options.Simplify { 396 | return path 397 | } 398 | return append(path, cm.buildWallyNode(s, site)) 399 | } 400 | 401 | func (cm *CallMapper) handleClosure(node *callgraph.Node, currentPath []wallynode.WallyNode) (*callgraph.Node, []wallynode.WallyNode) { 402 | newPath := cm.appendNodeToPath(node, currentPath, nil) 403 | 404 | if wallylib.IsClosure(node.Func) { 405 | node = cm.CallgraphNodes[node.Func.Parent()] 406 | for wallylib.IsClosure(node.Func) { 407 | if !cm.Options.Simplify { 408 | str := fmt.Sprintf("%s.[%s] %s", node.Func.Pkg.Pkg.Name(), node.Func.Name(), wallylib.GetFormattedPos(node.Func.Package(), node.Func.Pos())) 409 | newPath = append(newPath, cm.NodeFactory.CreateWallyNode(str, node, nil)) 410 | //str := fmt.Sprintf("%s.[%s] %s", node.Func.Pkg.Pkg.Name(), node.Func.Name(), wallylib.GetFormattedPos(node.Func.Package(), node.Func.Pos())) 411 | //newPath = append(newPath, wallynode.WallyNode{ 412 | // NodeString: str, 413 | // Caller: node, 414 | //}) 415 | } 416 | node = cm.CallgraphNodes[node.Func.Parent()] 417 | } 418 | } 419 | 420 | return node, newPath 421 | } 422 | 423 | func (cm *CallMapper) buildWallyNode(s *callgraph.Node, site ssa.CallInstruction) wallynode.WallyNode { 424 | if site == nil && cm.Options.Simplify { 425 | s = cm.getClosureRootNode(s) 426 | return cm.NodeFactory.CreateWallyNode("", s, site) 427 | } 428 | 429 | if cm.Options.PrintNodes || s.Func.Package() == nil { 430 | return cm.NodeFactory.CreateWallyNode(s.String(), s, site) 431 | } 432 | 433 | return cm.NodeFactory.CreateWallyNode("", s, site) 434 | } 435 | 436 | func (cm *CallMapper) getClosureRootNode(s *callgraph.Node) *callgraph.Node { 437 | if wallylib.IsClosure(s.Func) { 438 | node := cm.CallgraphNodes[s.Func.Parent()] 439 | for wallylib.IsClosure(node.Func) { 440 | node = cm.CallgraphNodes[node.Func.Parent()] 441 | } 442 | return node 443 | } 444 | return s 445 | } 446 | 447 | // Only to be used when debugging 448 | func printQueue(queue *list.List) { 449 | fmt.Println() 450 | fmt.Println() 451 | fmt.Println("Current Queue:") 452 | for e := queue.Front(); e != nil; e = e.Next() { 453 | bfsNode := e.Value.(BFSNode) 454 | fmt.Printf("Node: %s, Path: %v\n", bfsNode.Node.Func.Name(), bfsNode.Path) 455 | } 456 | fmt.Println("End of Queue") 457 | fmt.Println() 458 | } 459 | -------------------------------------------------------------------------------- /wallylib/core.go: -------------------------------------------------------------------------------- 1 | package wallylib 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/hex0punk/wally/indicator" 7 | "go/ast" 8 | "go/build" 9 | "go/types" 10 | "golang.org/x/tools/go/callgraph" 11 | "golang.org/x/tools/go/packages" 12 | "golang.org/x/tools/go/ssa" 13 | "strings" 14 | ) 15 | 16 | type FuncDecl struct { 17 | Pkg *types.Package 18 | Decl *ast.FuncDecl 19 | } 20 | 21 | func (f *FuncDecl) String() string { 22 | return fmt.Sprintf("%s.%s", f.Pkg.Name(), f.Decl.Name.String()) 23 | } 24 | 25 | type FuncInfo struct { 26 | Package string 27 | Pkg *types.Package 28 | Type string 29 | Name string 30 | Route string 31 | Signature *types.Signature 32 | EnclosedBy *FuncDecl 33 | } 34 | 35 | type SSAContext struct { 36 | EnclosedByFunc *ssa.Function 37 | Edges []*callgraph.Edge 38 | CallPaths [][]string 39 | } 40 | 41 | func (fi *FuncInfo) Match(indicators []indicator.Indicator) *indicator.Indicator { 42 | var match *indicator.Indicator 43 | 44 | for _, ind := range indicators { 45 | ind := ind 46 | 47 | // User may decide they do not care if the package matches. 48 | // It'd be worth adding a command to "take a guess" for potential routes 49 | if fi.Package != ind.Package && ind.Package != "*" { 50 | continue 51 | } 52 | if fi.Name != ind.Function { 53 | continue 54 | } 55 | 56 | if ind.ReceiverType != "" { 57 | if !fi.matchReceiver(ind.Package, ind.ReceiverType) { 58 | continue 59 | } 60 | } 61 | 62 | filterMatch := false 63 | if len(ind.MatchFilters) > 0 { 64 | for _, mf := range ind.MatchFilters { 65 | if mf != "" && fi.EnclosedBy.Pkg != nil { 66 | if strings.HasPrefix(fi.EnclosedBy.Pkg.Path(), mf) { 67 | filterMatch = true 68 | break 69 | } 70 | } 71 | } 72 | if !filterMatch { 73 | continue 74 | } 75 | } 76 | 77 | match = &ind 78 | } 79 | return match 80 | } 81 | 82 | func (fi *FuncInfo) matchReceiver(pkg, recvType string) bool { 83 | if fi.Signature == nil || fi.Signature.Recv() == nil { 84 | return false 85 | } 86 | 87 | recString := fmt.Sprintf("%s.%s", pkg, recvType) 88 | funcRecv := fi.Signature.Recv().Type().String() 89 | 90 | if recString == funcRecv || fmt.Sprintf("*%s", recString) == funcRecv { 91 | return true 92 | } 93 | return false 94 | } 95 | 96 | func GetFuncInfo(expr ast.Expr, info *types.Info) (*FuncInfo, error) { 97 | var funcIdent *ast.Ident 98 | var x ast.Expr 99 | 100 | switch funcExpr := expr.(type) { 101 | case *ast.Ident: 102 | funcIdent = funcExpr 103 | case *ast.SelectorExpr: 104 | funcIdent = funcExpr.Sel 105 | x = funcExpr.X 106 | default: 107 | return nil, errors.New("unable to get func data") 108 | } 109 | 110 | funcName := GetName(funcIdent) 111 | pkgPath, err := ResolvePackageFromIdent(funcIdent, info) 112 | if err != nil { 113 | if funcName != "" && x != nil { 114 | // Try to get pkg name from the selector, as this is likely not a pkg.func 115 | // but a struct.fun 116 | pkgPath, err = ResolvePackageFromIdent(x, info) 117 | if err != nil { 118 | return nil, err 119 | } 120 | } else { 121 | return nil, errors.New("unable to get func data") 122 | } 123 | } 124 | 125 | // TODO: maybe worth returning an error if we cannot get the signature, as we don't support 126 | // anonymous functions and closures as targetted functions via indicators anyway 127 | sig, _ := GetFuncSignature(funcIdent, info) 128 | 129 | return &FuncInfo{ 130 | Package: pkgPath.Path(), 131 | Pkg: pkgPath, 132 | //Type: nil, 133 | Name: funcName, 134 | Signature: sig, 135 | }, nil 136 | } 137 | 138 | func GetFuncSignature(expr ast.Expr, info *types.Info) (*types.Signature, error) { 139 | switch expr := expr.(type) { 140 | case *ast.Ident: 141 | obj := info.ObjectOf(expr) 142 | return getSignatureFromObject(obj) 143 | case *ast.CallExpr: 144 | if ident, ok := expr.Fun.(*ast.Ident); ok { 145 | obj := info.ObjectOf(ident) 146 | return getSignatureFromObject(obj) 147 | } 148 | } 149 | 150 | return nil, errors.New("unable to get signature from expression") 151 | } 152 | 153 | func getSignatureFromObject(obj types.Object) (*types.Signature, error) { 154 | switch obj := obj.(type) { 155 | case *types.Func: 156 | return obj.Type().(*types.Signature), nil 157 | case *types.Var: 158 | if sig, ok := obj.Type().(*types.Signature); ok { 159 | return sig, nil 160 | } 161 | } 162 | return nil, errors.New("object is not a function or does not have a signature") 163 | } 164 | 165 | func GetName(e ast.Expr) string { 166 | ident, ok := e.(*ast.Ident) 167 | if !ok { 168 | return "" 169 | } else { 170 | return ident.Name 171 | } 172 | } 173 | 174 | // TODO: Lots of repeated code that we can refactor here 175 | // Further, this is likely not sufficient if used for more general purposes (outside wally) as 176 | // there are parts of some statements (i.e. a ForStmt Post) that are not handled here 177 | func GetExprsFromStmt(stmt ast.Stmt) []*ast.CallExpr { 178 | var result []*ast.CallExpr 179 | switch s := stmt.(type) { 180 | case *ast.ExprStmt: 181 | ce := callExprFromExpr(s.X) 182 | if ce != nil { 183 | result = append(result, ce...) 184 | } 185 | case *ast.SwitchStmt: 186 | for _, iclause := range s.Body.List { 187 | clause := iclause.(*ast.CaseClause) 188 | for _, stm := range clause.Body { 189 | bodyExps := GetExprsFromStmt(stm) 190 | if len(bodyExps) > 0 { 191 | result = append(result, bodyExps...) 192 | } 193 | } 194 | } 195 | case *ast.IfStmt: 196 | condCe := callExprFromExpr(s.Cond) 197 | if condCe != nil { 198 | result = append(result, condCe...) 199 | } 200 | if s.Init != nil { 201 | initCe := GetExprsFromStmt(s.Init) 202 | if len(initCe) > 0 { 203 | result = append(result, initCe...) 204 | } 205 | } 206 | if s.Else != nil { 207 | elseCe := GetExprsFromStmt(s.Else) 208 | if len(elseCe) > 0 { 209 | result = append(result, elseCe...) 210 | } 211 | } 212 | ces := GetExprsFromStmt(s.Body) 213 | if len(ces) > 0 { 214 | result = append(result, ces...) 215 | } 216 | case *ast.BlockStmt: 217 | for _, stm := range s.List { 218 | ce := GetExprsFromStmt(stm) 219 | if ce != nil { 220 | result = append(result, ce...) 221 | } 222 | } 223 | case *ast.AssignStmt: 224 | for _, rhs := range s.Rhs { 225 | ce := callExprFromExpr(rhs) 226 | if ce != nil { 227 | result = append(result, ce...) 228 | } 229 | } 230 | for _, lhs := range s.Lhs { 231 | ce := callExprFromExpr(lhs) 232 | if ce != nil { 233 | result = append(result, ce...) 234 | } 235 | } 236 | case *ast.ReturnStmt: 237 | for _, retResult := range s.Results { 238 | ce := callExprFromExpr(retResult) 239 | if ce != nil { 240 | result = append(result, ce...) 241 | } 242 | } 243 | case *ast.ForStmt: 244 | ces := GetExprsFromStmt(s.Body) 245 | if len(ces) > 0 { 246 | result = append(result, ces...) 247 | } 248 | case *ast.RangeStmt: 249 | ces := GetExprsFromStmt(s.Body) 250 | if len(ces) > 0 { 251 | result = append(result, ces...) 252 | } 253 | case *ast.SelectStmt: 254 | for _, clause := range s.Body.List { 255 | //ces := GetExprsFromStmt(clause) 256 | if cc, ok := clause.(*ast.CommClause); ok { 257 | for _, stm := range cc.Body { 258 | bodyExps := GetExprsFromStmt(stm) 259 | if len(bodyExps) > 0 { 260 | result = append(result, bodyExps...) 261 | } 262 | } 263 | } 264 | } 265 | case *ast.LabeledStmt: 266 | ces := GetExprsFromStmt(s.Stmt) 267 | if len(ces) > 0 { 268 | result = append(result, ces...) 269 | } 270 | } 271 | return result 272 | } 273 | 274 | func callExprFromExpr(e ast.Expr) []*ast.CallExpr { 275 | switch e := e.(type) { 276 | case *ast.CallExpr: 277 | // This loop makes sure we obtain CEs when in the body function literal used 278 | // as arguments to CEs. See https://github.com/hashicorp/nomad/blob/d34788896f8892377a9039b81a65abd7a913b3cc/nomad/csi_endpoint.go#L1633 279 | // for an example 280 | for _, v := range e.Args { 281 | if rr, ok := v.(*ast.FuncLit); ok { 282 | return GetExprsFromStmt(rr.Body) 283 | } 284 | } 285 | return append([]*ast.CallExpr{}, e) 286 | case *ast.FuncLit: 287 | return GetExprsFromStmt(e.Body) 288 | } 289 | return nil 290 | } 291 | 292 | func GetFunctionFromCallInstruction(callInstr ssa.CallInstruction) *ssa.Function { 293 | callCommon := callInstr.Common() 294 | if callCommon == nil { 295 | return nil 296 | } 297 | 298 | return callCommon.StaticCallee() 299 | } 300 | 301 | func SiteMatchesFunc(site ssa.CallInstruction, function *ssa.Function) bool { 302 | callCommon := site.Common() 303 | if callCommon == nil { 304 | return false 305 | } 306 | 307 | siteFunc := GetFunctionFromSite(site) 308 | return siteFunc != nil && siteFunc == function || 309 | callCommon.Method != nil && callCommon.Method.Name() == function.Name() 310 | } 311 | 312 | func GetFunctionFromSite(site ssa.CallInstruction) *ssa.Function { 313 | callCommon := site.Common() 314 | if callCommon == nil { 315 | return nil 316 | } 317 | 318 | if !callCommon.IsInvoke() { 319 | return callCommon.StaticCallee() 320 | } else { 321 | receiverType := callCommon.Method.Type().(*types.Signature).Recv().Type() 322 | 323 | if ptrType, ok := receiverType.(*types.Pointer); ok { 324 | receiverType = ptrType.Elem() 325 | } 326 | 327 | // Get the method set of the receiver type 328 | methodSet := types.NewMethodSet(receiverType) 329 | for i := 0; i < methodSet.Len(); i++ { 330 | method := methodSet.At(i) 331 | if method.Obj().Name() == callCommon.Method.Name() { 332 | // Ensure method.Obj() is of type *types.Func 333 | if funcObj, ok := method.Obj().(*types.Func); ok { 334 | // Use the package's program to find the corresponding ssa.Function 335 | if fn := site.Parent().Prog.FuncValue(funcObj); fn != nil { 336 | return fn 337 | } 338 | } 339 | } 340 | } 341 | 342 | return nil 343 | } 344 | } 345 | 346 | func IsClosure(function *ssa.Function) bool { 347 | return strings.Contains(function.Name(), "$") 348 | } 349 | 350 | func getModuleName(pkg *packages.Package) (string, error) { 351 | if pkg.Module != nil { 352 | return pkg.Module.Path, nil 353 | } 354 | return "", fmt.Errorf("module not found for package %s", pkg.PkgPath) 355 | } 356 | 357 | func inStd(node *callgraph.Node) bool { 358 | pkg, _ := build.Import(node.Func.Pkg.Pkg.Path(), "", 0) 359 | return pkg.Goroot 360 | } 361 | -------------------------------------------------------------------------------- /wallylib/resolvers.go: -------------------------------------------------------------------------------- 1 | package wallylib 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/hex0punk/wally/checker" 7 | "github.com/hex0punk/wally/indicator" 8 | "go/ast" 9 | "go/types" 10 | "golang.org/x/tools/go/analysis" 11 | ) 12 | 13 | func ResolveParams(params []indicator.RouteParam, sig *types.Signature, ce *ast.CallExpr, pass *analysis.Pass) map[string]string { 14 | resolvedParams := make(map[string]string) 15 | for _, param := range params { 16 | param := param 17 | val := "" 18 | if param.Name != "" && sig != nil { 19 | val = ResolveParamFromName(param.Name, sig, ce, pass) 20 | } else { 21 | val = ResolveParamFromPos(param.Pos, ce, pass) 22 | } 23 | resolvedParams[param.Name] = val 24 | } 25 | return resolvedParams 26 | } 27 | 28 | func ResolveParamFromPos(pos int, param *ast.CallExpr, pass *analysis.Pass) string { 29 | if param.Args != nil && len(param.Args) > 0 { 30 | arg := param.Args[pos] 31 | return GetValueFromExp(arg, pass) 32 | } 33 | return "" 34 | } 35 | 36 | func ResolveParamFromName(name string, sig *types.Signature, param *ast.CallExpr, pass *analysis.Pass) string { 37 | // First get the pos for the arg 38 | pos, err := GetParamPos(sig, name) 39 | if err != nil { 40 | // we failed at getting param, return an empty string and be sad (for now) 41 | return "" 42 | } 43 | 44 | return ResolveParamFromPos(pos, param, pass) 45 | } 46 | 47 | func GetParamPos(sig *types.Signature, paramName string) (int, error) { 48 | numParams := sig.Params().Len() 49 | for i := 0; i < numParams; i++ { 50 | param := sig.Params().At(i) 51 | if param.Name() == paramName { 52 | return i, nil 53 | } 54 | } 55 | // TODO: not great to return 0 56 | return 0, errors.New("unable to find param pos") 57 | } 58 | 59 | func GetValueFromExp(exp ast.Expr, pass *analysis.Pass) string { 60 | // This should actually be called only AFTER we have checked facts to see if the value was already obtained, 61 | // otherwise this could be double work for nothing. That way we also don't need to pass a Pass to so many 62 | // funcs here, andinstead can stick to packages.TypesInfo 63 | info := pass.TypesInfo 64 | switch node := exp.(type) { 65 | case *ast.BasicLit: // i.e. "/thepath" 66 | return node.Value 67 | case *ast.SelectorExpr: // i.e. "paths.User" where User is a constant 68 | // If its a constant its a selector and we can extract the value below 69 | o1 := info.ObjectOf(node.Sel) 70 | // TODO: Write a func for this 71 | if con, ok := o1.(*types.Const); ok { 72 | return con.Val().String() 73 | } 74 | if con, ok := o1.(*types.Var); ok { 75 | // Check if global 76 | var fact checker.GlobalVar 77 | if pass.ImportObjectFact(o1, &fact) { 78 | return fact.Val 79 | } 80 | // A non-constant value, best effort (without ssa navigator) is to 81 | // return the variable name 82 | return fmt.Sprintf("", GetName(node.X), con.Id()) 83 | } 84 | case *ast.Ident: // i.e. user where user is a const 85 | o1 := info.ObjectOf(node) 86 | // TODO: Write a func for this 87 | if con, ok := o1.(*types.Const); ok { 88 | return con.Val().String() 89 | } 90 | 91 | // Likely a local var 92 | if con, ok := o1.(*types.Var); ok { 93 | var fact checker.LocalVar 94 | if pass.ImportObjectFact(o1, &fact) { 95 | var result string 96 | for i, v := range fact.Vals { 97 | if i == len(fact.Vals)-1 { 98 | result += " " + v 99 | continue 100 | } 101 | result += v + " || " 102 | } 103 | return result 104 | } 105 | // A non-constant value, best effort (without ssa navigator) is to 106 | // return the variable name 107 | return fmt.Sprintf("", node.Name, con.Id()) 108 | } 109 | case *ast.CompositeLit: // i.e. []string{"POST"} 110 | vals := "" 111 | for _, lit := range node.Elts { 112 | val := GetValueFromExp(lit, pass) 113 | vals = vals + " " + val 114 | } 115 | return vals 116 | case *ast.BinaryExpr: // i.e. base+"/getUser" 117 | left := GetValueFromExp(node.X, pass) 118 | right := GetValueFromExp(node.Y, pass) 119 | if left == "" { 120 | left = "" 121 | } 122 | if right == "" { 123 | right = "" 124 | } 125 | // We assume the operator (be.Op) is +, because why would it be anything else 126 | // for a func param 127 | return left + right 128 | } 129 | return "" 130 | } 131 | 132 | // ResolvePackageFromIdent TODO: This may be useful to get receiver type of func 133 | // Also, wrong name, its from an Expr, not from Idt, technically 134 | func ResolvePackageFromIdent(expr ast.Expr, info *types.Info) (*types.Package, error) { 135 | idt, ok := expr.(*ast.Ident) 136 | if !ok { 137 | return nil, errors.New("not an ident") 138 | } 139 | 140 | o1 := info.ObjectOf(idt) 141 | if o1 != nil && o1.Pkg() != nil { 142 | // TODO: Can also get the plain pkg name without path with `o1.Pkg().Name()` 143 | return o1.Pkg(), nil 144 | } 145 | 146 | return nil, errors.New("unable to get package name from Ident") 147 | } 148 | -------------------------------------------------------------------------------- /wallylib/util.go: -------------------------------------------------------------------------------- 1 | package wallylib 2 | 3 | import ( 4 | "fmt" 5 | "go/token" 6 | "go/types" 7 | "golang.org/x/tools/go/ssa" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | func DedupPaths(paths [][]string) [][]string { 13 | result := [][]string{} 14 | for _, path := range paths { 15 | duplicate := false 16 | for _, existingPath := range result { 17 | if Equal(path, existingPath) { 18 | duplicate = true 19 | break 20 | } 21 | } 22 | if !duplicate { 23 | result = append(result, path) 24 | } 25 | } 26 | return result 27 | } 28 | 29 | func Equal(a, b []string) bool { 30 | if len(a) != len(b) { 31 | return false 32 | } 33 | for i, x := range a { 34 | if x != b[i] { 35 | return false 36 | } 37 | } 38 | return true 39 | } 40 | 41 | // Copied from https://github.com/golang/tools/blob/7e4a1ff3b7ea212d372df3899fefe235a20064cc/refactor/rename/util.go#L59 42 | func IsLocal(obj types.Object) bool { 43 | // [... 5=stmt 4=func 3=file 2=pkg 1=universe] 44 | if obj == nil { 45 | return false 46 | } 47 | var depth int 48 | for scope := obj.Parent(); scope != nil; scope = scope.Parent() { 49 | depth++ 50 | } 51 | return depth >= 4 52 | } 53 | 54 | func GetFormattedPos(pkg *ssa.Package, pos token.Pos) string { 55 | fs := pkg.Prog.Fset 56 | p := fs.Position(pos) 57 | currentPath, _ := os.Getwd() 58 | relPath, _ := filepath.Rel(currentPath, p.Filename) 59 | return fmt.Sprintf("%s:%d:%d", relPath, p.Line, p.Column) 60 | } 61 | -------------------------------------------------------------------------------- /wallynode/factory.go: -------------------------------------------------------------------------------- 1 | package wallynode 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hex0punk/wally/wallylib" 6 | "golang.org/x/tools/go/callgraph" 7 | "golang.org/x/tools/go/ssa" 8 | ) 9 | 10 | type WallyNodeFactory struct { 11 | CallgraphNodes map[*ssa.Function]*callgraph.Node 12 | } 13 | 14 | func NewWallyNodeFactory(callGraphnodes map[*ssa.Function]*callgraph.Node) *WallyNodeFactory { 15 | return &WallyNodeFactory{ 16 | CallgraphNodes: callGraphnodes, 17 | } 18 | } 19 | 20 | func (f *WallyNodeFactory) CreateWallyNode(nodeStr string, caller *callgraph.Node, site ssa.CallInstruction) WallyNode { 21 | recoverable := false 22 | if nodeStr == "" { 23 | if site == nil { 24 | nodeStr = fmt.Sprintf("Func: %s.[%s] %s", caller.Func.Pkg.Pkg.Name(), caller.Func.Name(), wallylib.GetFormattedPos(caller.Func.Package(), caller.Func.Pos())) 25 | } else { 26 | fp := wallylib.GetFormattedPos(caller.Func.Package(), site.Pos()) 27 | recoverable = IsRecoverable(caller, f.CallgraphNodes) 28 | nodeStr = GetNodeString(fp, caller, recoverable) 29 | } 30 | } 31 | return WallyNode{ 32 | NodeString: nodeStr, 33 | Caller: caller, 34 | Site: site, 35 | recoverable: recoverable, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /wallynode/wallynode.go: -------------------------------------------------------------------------------- 1 | package wallynode 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/hex0punk/wally/wallylib" 7 | "golang.org/x/tools/go/callgraph" 8 | "golang.org/x/tools/go/ssa" 9 | ) 10 | 11 | type WallyNode struct { 12 | NodeString string 13 | Caller *callgraph.Node 14 | Site ssa.CallInstruction 15 | recoverable bool 16 | } 17 | 18 | type NodeType int 19 | 20 | const ( 21 | Site NodeType = iota 22 | Function 23 | ) 24 | 25 | func (n *WallyNode) IsRecoverable() bool { 26 | return n.recoverable 27 | } 28 | 29 | func GetNodeString(basePos string, s *callgraph.Node, recoverable bool) string { 30 | pkg := s.Func.Package() 31 | function := s.Func 32 | baseStr := fmt.Sprintf("%s.[%s] %s", pkg.Pkg.Name(), function.Name(), basePos) 33 | 34 | if recoverable { 35 | return fmt.Sprintf("%s.[%s] (recoverable) %s", pkg.Pkg.Name(), function.Name(), basePos) 36 | } 37 | 38 | return baseStr 39 | } 40 | 41 | func IsRecoverable(s *callgraph.Node, callgraphNodes map[*ssa.Function]*callgraph.Node) bool { 42 | function := s.Func 43 | if function.Recover != nil { 44 | rec, err := findDeferRecover(function, function.Recover.Index-1) 45 | if err == nil && rec { 46 | return true 47 | } 48 | } 49 | if wallylib.IsClosure(function) { 50 | enclosingFunc := closureArgumentOf(s, callgraphNodes[s.Func.Parent()]) 51 | if enclosingFunc != nil && enclosingFunc.Recover != nil { 52 | rec, err := findDeferRecover(enclosingFunc, enclosingFunc.Recover.Index-1) 53 | if err == nil && rec { 54 | return true 55 | } 56 | } 57 | if enclosingFunc != nil { 58 | for _, af := range enclosingFunc.AnonFuncs { 59 | if af.Recover != nil { 60 | rec, err := findDeferRecover(af, af.Recover.Index-1) 61 | if err == nil && rec { 62 | return true 63 | } 64 | } 65 | } 66 | } 67 | } 68 | return false 69 | } 70 | 71 | func findDeferRecover(fn *ssa.Function, idx int) (bool, error) { 72 | visited := make(map[*ssa.Function]bool) 73 | return findDeferRecoverRecursive(fn, visited, idx) 74 | } 75 | 76 | func findDeferRecoverRecursive(fn *ssa.Function, visited map[*ssa.Function]bool, starterBlock int) (bool, error) { 77 | if visited[fn] { 78 | return false, nil 79 | } 80 | 81 | visited[fn] = true 82 | 83 | // we use starterBlock on first call as we know where the defer call is, then reset it to 0 for subsequent blocks 84 | // to find the recover() if there 85 | for blockIdx := starterBlock; blockIdx < len(fn.Blocks); blockIdx++ { 86 | block := fn.Blocks[blockIdx] 87 | for _, instr := range block.Instrs { 88 | switch it := instr.(type) { 89 | case *ssa.Defer: 90 | if call, ok := it.Call.Value.(*ssa.Function); ok { 91 | if containsRecoverCall(call) { 92 | return true, nil 93 | } 94 | } 95 | case *ssa.Go: 96 | if call, ok := it.Call.Value.(*ssa.Function); ok { 97 | if containsRecoverCall(call) { 98 | return true, nil 99 | } 100 | } 101 | case *ssa.Call: 102 | if callee := it.Call.Value; callee != nil { 103 | if callee.Name() == "recover" { 104 | return true, nil 105 | } 106 | if nestedFunc, ok := callee.(*ssa.Function); ok { 107 | if _, err := findDeferRecoverRecursive(nestedFunc, visited, 0); err != nil { 108 | return true, nil 109 | } 110 | } 111 | } 112 | case *ssa.MakeClosure: 113 | if closureFn, ok := it.Fn.(*ssa.Function); ok { 114 | res, err := findDeferRecoverRecursive(closureFn, visited, 0) 115 | if err != nil { 116 | return false, errors.New("unexpected error finding recover block") 117 | } 118 | if res { 119 | return true, nil 120 | } 121 | } 122 | } 123 | } 124 | } 125 | return false, nil 126 | } 127 | 128 | func containsRecoverCall(fn *ssa.Function) bool { 129 | for _, block := range fn.Blocks { 130 | for _, instr := range block.Instrs { 131 | if isRecoverCall(instr) { 132 | return true 133 | } 134 | } 135 | } 136 | return false 137 | } 138 | 139 | func isRecoverCall(instr ssa.Instruction) bool { 140 | if callInstr, ok := instr.(*ssa.Call); ok { 141 | if callee, ok := callInstr.Call.Value.(*ssa.Builtin); ok { 142 | return callee.Name() == "recover" 143 | } 144 | } 145 | return false 146 | } 147 | 148 | // closureArgumentOf checks if the function is passed as an argument to another function 149 | // and returns the enclosing function 150 | func closureArgumentOf(targetNode *callgraph.Node, edges *callgraph.Node) *ssa.Function { 151 | for _, edge := range edges.Out { 152 | for _, arg := range edge.Site.Common().Args { 153 | if argFn, ok := arg.(*ssa.MakeClosure); ok { 154 | if argFn.Fn == targetNode.Func { 155 | if res, ok := edge.Site.Common().Value.(*ssa.Function); ok { 156 | return res 157 | } 158 | } 159 | } 160 | } 161 | } 162 | return nil 163 | } 164 | --------------------------------------------------------------------------------