├── .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 |
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 | 
366 | Finding node. This is a node discovered via wally indicators. Every finding node is the end of a path
367 |
368 | 
369 | This node is the root of a path to a finding node.
370 |
371 | 
372 | Intermediate node between a root and a finding node.
373 |
374 | 
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 | 
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
Pkg: Func: Params: Enclosed By: Pos: No. of Paths:
--------------------------------------------------------------------------------
/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 |
22 | Pkg:
23 | Func:
24 | Params:
25 | Enclosed By:
26 | Pos:
27 | No. of Paths:
28 |
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 |
--------------------------------------------------------------------------------