├── .dockerignore ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── agent ├── agent.go ├── crash.go ├── go.mod ├── go.sum ├── job.go ├── main.go ├── project.go ├── stats.go └── utils.go ├── hooks └── build └── server ├── agent.go ├── alert.go ├── auth.go ├── auth_test.go ├── crash.go ├── data └── .gitignore ├── db.go ├── db_test.go ├── go.mod ├── go.sum ├── job.go ├── main.go ├── main_test.go ├── plot.go ├── public ├── plots │ └── .gitignore └── static │ ├── css │ ├── bootstrap.min.css │ ├── bootstrap.min.css.map │ ├── custom.css │ └── fontawesome-all.min.css │ ├── gif │ └── demo.gif │ ├── js │ ├── bootstrap.bundle.min.js │ ├── bootstrap.bundle.min.js.map │ ├── bootstrap.min.js │ ├── bootstrap.min.js.map │ ├── custom.js │ ├── jquery-succinct-min.js │ ├── jquery.min.js │ └── popper.min.js │ ├── png │ └── schema.png │ ├── svg │ ├── bootstrap-icons.svg │ └── carrot-solid.svg │ └── webfonts │ ├── fa-brands-400.eot │ ├── fa-brands-400.svg │ ├── fa-brands-400.ttf │ ├── fa-brands-400.woff │ ├── fa-brands-400.woff2 │ ├── fa-regular-400.eot │ ├── fa-regular-400.svg │ ├── fa-regular-400.ttf │ ├── fa-regular-400.woff │ ├── fa-regular-400.woff2 │ ├── fa-solid-900.eot │ ├── fa-solid-900.svg │ ├── fa-solid-900.ttf │ ├── fa-solid-900.woff │ └── fa-solid-900.woff2 ├── server.go ├── stat.go ├── status.go ├── templates ├── includes │ ├── 404.html │ ├── agent_edit.html │ ├── agents_create.html │ ├── agents_view.html │ ├── crash_edit.html │ ├── crashes_view.html │ ├── home.html │ ├── job_edit.html │ ├── job_plot.html │ ├── job_view.html │ ├── jobs_create.html │ ├── jobs_upload.html │ ├── jobs_view.html │ └── user_edit.html ├── layouts │ └── base.html └── user_login.html ├── user.go ├── user_test.go └── utils.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio Code 2 | .vscode/* 3 | !.vscode/settings.json 4 | !.vscode/tasks.json 5 | !.vscode/launch.json 6 | !.vscode/extensions.json 7 | *.code-workspace 8 | 9 | # Local History for Visual Studio Code 10 | .history/ 11 | 12 | # Binaries for programs and plugins 13 | *.exe 14 | *.exe~ 15 | *.dll 16 | *.so 17 | *.dylib 18 | 19 | # Test binary, built with `go test -c` 20 | *.test 21 | 22 | # Output of the go coverage tool, specifically when used with LiteIDE 23 | *.out 24 | 25 | # Dependency directories (remove the comment below to include it) 26 | # vendor/ 27 | 28 | # Misc 29 | *.stg 30 | *.graphml 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.6.1] - 2025-04-20 10 | ### Changed 11 | - Tidied go.mod. 12 | - Update Docker base images. 13 | 14 | ## [0.6.0] - 2025-04-20 15 | ### Added 16 | - Support for custom bug bucket feature. 17 | - Support for 40 parallel fuzzer instances. 18 | - Variable mode to allow for varying parameters. 19 | - Sequential mode to allow execution of numbered target applications. 20 | 21 | ### Changed 22 | - Infer binary directory from target architecture. 23 | - Default risk set to none. 24 | 25 | ### Fixed 26 | - Remove usage of deprecated io/ioutil package. 27 | 28 | ## [0.5.0] - 2022-06-08 29 | ### Added 30 | - Ability to export/import jobs. 31 | - Periodic email alerts of new crashes. 32 | - Button to purge crash records from database. 33 | 34 | ### Changed 35 | - Update Bootstrap icons to v1.8.3. 36 | - Update button style for jobs. 37 | - Update Docker base images. 38 | 39 | ## [0.4.0] - 2022-02-05 40 | ### Changed 41 | - Type of fID changed from string to integer. 42 | 43 | ## [0.3.1] - 2022-02-02 44 | ### Changed 45 | - More specific regex pattern for crash detection. 46 | 47 | ### Fixed 48 | - Replace invalid execs_per_sec value for unmarshaling. 49 | - Ignore unspecified or zero value for memory limit. 50 | 51 | ## [0.3.0] - 2021-12-18 52 | ### Added 53 | - Support for using WinAFL as a pre-configured tool for DynamoRIO. 54 | - Support for afl-fuzz environment variables and autoresume. 55 | 56 | ### Changed 57 | - Update fuzzing job templates. 58 | 59 | ### Fixed 60 | - Purging of crash recrods from the database. 61 | - Handle error when Python path is incorrect. 62 | - Use the randomly generated shared memory name. 63 | 64 | ## [0.2.0] - 2021-11-02 65 | ### Added 66 | - Support for additional afl-fuzz options. 67 | 68 | ### Fixed 69 | - Minor template issues. 70 | - Check if process was found before killing it. 71 | 72 | ## [0.1.0] - 2021-10-10 73 | ### Added 74 | - Support for sample delivery via shared memory. 75 | 76 | ### Changed 77 | - Set coverage type values in job creation form. 78 | - Update build constraints. 79 | - Migrate structable from Masterminds to sgabe. 80 | 81 | ### Fixed 82 | - Increase the width of the search box. 83 | - Overflowing card header. 84 | - Hide pager for single page. 85 | 86 | ## [0.0.7] - 2021-10-02 87 | ### Changed 88 | - Gin version bumped to fix CVE-2020-28483. 89 | 90 | ## [0.0.6] - 2021-04-10 91 | ### Added 92 | - Common template functions provided by Sprig. 93 | - Pagination to navigate through the crashes. 94 | - Search box to filter cards. 95 | 96 | ## [0.0.5] - 2021-04-01 97 | ### Fixed 98 | - Prevent erroneous user profile update. 99 | - jQuery AJAX used to download binary crash samples. 100 | 101 | ## [0.0.4] - 2021-03-22 102 | ### Added 103 | - Flag to enable debug mode and non-secure session cookie. 104 | - Show bitmap coverage information among overall results. 105 | 106 | ### Fixed 107 | - Show target method when offset is not specified. 108 | - Binding to command line host and port flags. 109 | - Anonymous function as parameter to setTimeout(). 110 | 111 | ### Changed 112 | - Allow running up to 20 fuzzer instances simultaneously. 113 | - Reload the page after successfully starting a job. 114 | - Use goroutine to read process's standard output. 115 | - More specific regex pattern to find crash samples. 116 | 117 | ## [0.0.3] - 2021-01-24 118 | ### Added 119 | - Support additional command line arguments for target application. 120 | - Support for absolute paths for input and output. 121 | 122 | ### Fixed 123 | - Fix regex pattern used to extract crash location from BugId output. 124 | - Return early on invalid number of PIDs provided for checking a job. 125 | - Missing instrumentation option to set target_offset. 126 | 127 | ### Changed 128 | - Use smaller font size in footer for mobile screens. 129 | - Allow crash analysis when page heap is not enabled. 130 | - Allow running up to 8 fuzzer instances simultaneously. 131 | - Sort crashes in descending order by internal ID. 132 | - Update crash file paths when resuming aborted jobs. 133 | - Increase request timeout to avoid errors when starting jobs. 134 | - Increase database query limit to display more crashes. 135 | - Refactor crash template. 136 | - Improve regex pattern to detect system errors. 137 | 138 | ### Removed 139 | - Unused id attributes in the HTML templates. 140 | - Unused CSS stylesheet. 141 | 142 | ## [0.0.2] - 2020-12-14 143 | ### Removed 144 | - Unnecessary debug print. 145 | 146 | ## [0.0.1] - 2020-12-14 147 | ### Added 148 | - Initial commit. 149 | 150 | ### Changed 151 | - Redirect logged in users to jobs when page was not found. 152 | - Improved template renderer to use layouts. 153 | 154 | [Unreleased]: https://github.com/sgabe/winaflpet/compare/v0.6.1...HEAD 155 | [0.6.1]: https://github.com/sgabe/winaflpet/releases/tag/v0.6.1 156 | [0.6.0]: https://github.com/sgabe/winaflpet/releases/tag/v0.6.0 157 | [0.5.0]: https://github.com/sgabe/winaflpet/releases/tag/v0.5.0 158 | [0.4.0]: https://github.com/sgabe/winaflpet/releases/tag/v0.4.0 159 | [0.3.1]: https://github.com/sgabe/winaflpet/releases/tag/v0.3.1 160 | [0.3.0]: https://github.com/sgabe/winaflpet/releases/tag/v0.3.0 161 | [0.2.0]: https://github.com/sgabe/winaflpet/releases/tag/v0.2.0 162 | [0.1.0]: https://github.com/sgabe/winaflpet/releases/tag/v0.1.0 163 | [0.0.7]: https://github.com/sgabe/winaflpet/releases/tag/v0.0.7 164 | [0.0.6]: https://github.com/sgabe/winaflpet/releases/tag/v0.0.6 165 | [0.0.5]: https://github.com/sgabe/winaflpet/releases/tag/v0.0.5 166 | [0.0.4]: https://github.com/sgabe/winaflpet/releases/tag/v0.0.4 167 | [0.0.3]: https://github.com/sgabe/winaflpet/releases/tag/v0.0.3 168 | [0.0.2]: https://github.com/sgabe/winaflpet/releases/tag/v0.0.2 169 | [0.0.1]: https://github.com/sgabe/winaflpet/releases/tag/v0.0.1 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang@sha256:7772cb5322baa875edd74705556d08f0eeca7b9c4b5367754ce3f2f00041ccee as builder 2 | 3 | ARG BUILD_VER 4 | ARG BUILD_REV 5 | ARG BUILD_DATE 6 | 7 | ENV BUILD_VER ${BUILD_VER} 8 | ENV BUILD_REV ${BUILD_REV} 9 | ENV BUILD_DATE ${BUILD_DATE} 10 | ENV GO111MODULE=on 11 | ENV USER=winaflpet 12 | ENV UID=10001 13 | 14 | LABEL org.label-schema.build-date=$BUILD_DATE \ 15 | org.label-schema.vcs-url="https://github.com/sgabe/winaflpet.git" \ 16 | org.label-schema.vcs-ref=$BUILD_REV \ 17 | org.label-schema.schema-version="1.0.0-rc1" 18 | 19 | COPY . /tmp/winaflpet/ 20 | 21 | RUN apk update && \ 22 | apk add --no-cache git ca-certificates tzdata gnuplot libc-dev gcc && \ 23 | update-ca-certificates && \ 24 | adduser --disabled-password \ 25 | --gecos "" \ 26 | --home "/nonexistent" \ 27 | --shell "/sbin/nologin" \ 28 | --no-create-home \ 29 | --uid "${UID}" "${USER}" && \ 30 | cd /tmp/winaflpet/server && \ 31 | go get -d -v . && \ 32 | CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" GOOS=linux GOARCH=amd64 go build \ 33 | -ldflags="-X main.BuildVer=$BUILD_VER -X main.BuildRev=$BUILD_REV -w -s -extldflags '-static'" -a \ 34 | -o /tmp/winaflpet/winaflpet . 35 | 36 | FROM alpine@sha256:1c4eef651f65e2f7daee7ee785882ac164b02b78fb74503052a26dc061c90474 37 | 38 | RUN apk update && \ 39 | apk add --no-cache curl gnuplot 40 | 41 | COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo 42 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 43 | COPY --from=builder /etc/passwd /etc/passwd 44 | COPY --from=builder /etc/group /etc/group 45 | 46 | COPY --from=builder --chown=winaflpet:winaflpet /tmp/winaflpet/server/public /opt/winaflpet/public 47 | COPY --from=builder /tmp/winaflpet/server/templates /opt/winaflpet/templates 48 | COPY --from=builder /tmp/winaflpet/winaflpet /opt/winaflpet/ 49 | 50 | HEALTHCHECK --start-period=1m \ 51 | CMD curl --silent --fail -X POST http://127.0.0.1:4141/ping || exit 1 52 | 53 | VOLUME /data 54 | 55 | EXPOSE 4141 56 | 57 | WORKDIR /opt/winaflpet 58 | 59 | USER winaflpet:winaflpet 60 | 61 | ENTRYPOINT ["/opt/winaflpet/winaflpet"] 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Gabor Seljan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all server agent 2 | 3 | BUILD_VER := 0.6.1 4 | BUILD_REV := $(shell git rev-parse --short HEAD) 5 | BUILD_DATE ?= $(shell git log --pretty=format:%ct -1) 6 | 7 | BUILD_VER_VAR := main.BuildVer 8 | BUILD_REV_VAR := main.BuildRev 9 | LDFLAGS := -X \"$(BUILD_VER_VAR)=$(BUILD_VER)\" \ 10 | -X \"$(BUILD_REV_VAR)=$(BUILD_REV)\" \ 11 | 12 | server: 13 | ifeq ($(OS),Windows_NT) 14 | go build -ldflags "$(LDFLAGS)" -o ./winaflpet-server.exe ./server 15 | else 16 | docker build \ 17 | --build-arg BUILD_VER=$(BUILD_VER) \ 18 | --build-arg BUILD_REV=$(BUILD_REV) \ 19 | --build-arg BUILD_DATE=$(BUILD_DATE) \ 20 | --no-cache -t sgabe/winaflpet:$(BUILD_VER) . 21 | endif 22 | 23 | agent: 24 | go build -ldflags "$(LDFLAGS)" -o ./winaflpet-agent.exe ./agent 25 | 26 | clean: 27 | go clean && del winaflpet-server.exe && del winaflpet-agent.exe 28 | 29 | all: 30 | server 31 | agent 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WinAFL Pet 2 | 3 | [![GitLab pipeline status](https://img.shields.io/gitlab/pipeline/sgabe/winaflpet/main)](https://gitlab.com/sgabe/winaflpet/-/pipelines) 4 | [![Docker Automated build](https://img.shields.io/docker/automated/sgabe/winaflpet)](https://hub.docker.com/r/sgabe/winaflpet/builds) 5 | [![Docker Image Size (tag)](https://img.shields.io/docker/image-size/sgabe/winaflpet/latest)](https://hub.docker.com/r/sgabe/winaflpet) 6 | [![GitHub](https://img.shields.io/github/license/sgabe/winaflpet)](LICENSE) 7 | 8 | **WinAFL Pet** is a web user interface dedicated to [WinAFL](https://github.com/googleprojectzero/winafl) remote management via an agent running as a system service on fuzzing machines. The purpose of this project is to allow easy monitoring of fuzzing jobs running on several remote machines. Typical use case is to run the *server* component on a NAS or Raspberry PI and deploy *agents* on a virtualization server as you like. The below figure shows this typical deployment scenario. 9 | 10 | !["WinAFL Pet schema diagram"](server/public/static/png/schema.png "WinAFL Pet schema") 11 | 12 | ## Demo 13 | 14 | ![WinAFL Pet demo screencapture](server/public/static/gif/demo.gif "WinAFL Pet demo") 15 | 16 | ## Requirements 17 | 18 | The following tools must be available on the fuzzing machine. It is recommended to install all the tools in a single directory (e.g. `C:\Tools\...`) for easier management. In general, if WinAFL runs fine manually, should be also fine when run by the agent. In fact, start fuzzing manually and continue with the agent once everything is set up correctly. 19 | 20 | + [WinAFL](https://github.com/googleprojectzero/winafl) for fuzzing 21 | + [DynamoRIO](https://github.com/googleprojectzero/winafl) for instrumentation 22 | + [BugID](https://github.com/SkyLined/BugId) for crash analysis 23 | - [Python 2.7.14](https://www.python.org/downloads/release/python-2714/) 24 | - [Debugging Tools for Windows](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/) 25 | 26 | ## Deployment 27 | 28 | ### Server 29 | 30 | The server is written in [Go](https://golang.org/) using the [Gin](https://github.com/gin-gonic/gin) web framework and it is running in a minimal [Docker](https://www.docker.com/) image based on [Alpine Linux](https://hub.docker.com/_/alpine). You could use the following command to start a container with persistent data storage: 31 | 32 | > docker run -p 127.0.0.1:4141:4141 \ 33 | -v /path/to/winaflpet/data:/opt/winaflpet/data \ 34 | sgabe/winaflpet 35 | 36 | ### Agent 37 | 38 | The agent is also written in [Go](https://golang.org/) and designed for minimal footprint. Currently it uses the **Windows Credential Vault** to store an automatically generated API key. A service account with *Log on as a service* permission is necessary to retrieve the API key from the vault. See the FAQ for more information. Note down the key as it will be necessary to create a new agent on the management interface. 39 | 40 | > winaflpet-agent.exe --service install 41 | Username of service account: fuzzy\gabor 42 | Password of service account: ******** 43 | Secret key of service account: 44 | > winaflpet-agent.exe --service start 45 | 46 | ## Usage 47 | 48 | Currently the default user is *admin* with the hostname or Docker container ID as password. Do not forget to change the default password after logging in. Follow the below steps to start fuzzing: 49 | 50 | 1. Go to the **Agents** page and create a new agent using the previously generated secret key. 51 | 2. Go to the **Jobs** page and create a new job associated with the agent created in the previous step. 52 | 3. *Start* a fuzzing instance by clicking on the ![play](https://icons.getbootstrap.com/icons/play-fill.svg) icon. 53 | 4. Be patient until the start request completes (and WinAFL finishes the dry-run). 54 | 5. *View* statistics by clicking on the ![eye](https://icons.getbootstrap.com/icons/eye-fill.svg) icon. 55 | 6. *Check* running instances by clicking on the ![circle](https://icons.getbootstrap.com/icons/check-circle-fill.svg) icon. 56 | 7. *Collect* crash data by clicking on the ![cloud](https://icons.getbootstrap.com/icons/cloud-arrow-down-fill.svg) icon. 57 | 8. Go to the **Crashes** page to *verify* new crashes by clicking on the ![pencil](https://icons.getbootstrap.com/icons/check-square-fill.svg) icon. 58 | 9. Go to the **Jobs** page and *stop* all fuzzing instances by clicking on the ![stop](https://icons.getbootstrap.com/icons/stop-fill.svg) icon. 59 | 60 | ### Sample delivery 61 | 62 | WinAFL Pet supports delivering samples via shared memory and via a file. You need to make sure that your harness understands the `-f ` argument for file mode and the `-s ` argument for shared memory mode. Note that even if you are using shared memory for fuzzing, your harness must support file mode for analyzing crashes with BugId. The `-f` argument will be automatically passed to the testing harness. 63 | 64 | ## Environment variables 65 | 66 | Some of the configuration options are exposed via environment variables to be used in the container. This allows you to customize WinAFL Pet without creating or modifying configuration files. The below table summarizes the available environment variables and their default settings. 67 | 68 | | Variable | Default | 69 | | -------------------------------------|---------------------------------------| 70 | | `WINAFLPET_DATA` | `data` | 71 | | `WINAFLPET_HOST` | `127.0.0.1` | 72 | | `WINAFLPET_PORT` | `4141` | 73 | | `WINAFLPET_LOG` | `winaflpet.log` | 74 | 75 | ## Building WinAFL Pet 76 | 77 | You can build the server in a Docker container on Linux: 78 | 79 | make server 80 | 81 | Or the service binary for the agent on Windows: 82 | 83 | make agent 84 | 85 | ## FAQ 86 | 87 | ### How do I configure a user account to have *Logon as a service* permission? 88 | Perform the following to edit the *Local Security Policy* of the computer where you want to fuzz: 89 | 90 | 1. Open the **Local Security Policy**. 91 | 2. Expand **Local Policies** and click on **User Rights Assignment**. 92 | 3. In the right pane, double-click *Log on as a service*. 93 | 4. Click on the **Add User or Group...** button to add the new user. 94 | 5. In the **Select Users or Groups** dialogue, find the user you wish to enter and click **OK**. 95 | 6. Click **OK** in the **Log on as a service Properties** to save changes. 96 | 97 | Ensure that the user which you have added above is not listed in the *Deny log on as a service* policy in the **Local Security Policy**. 98 | -------------------------------------------------------------------------------- /agent/agent.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/kardianos/service" 14 | "github.com/pjebs/restgate" 15 | ) 16 | 17 | var ( 18 | isFirstRun bool = true 19 | project Project 20 | ) 21 | 22 | type Agent struct { 23 | Server http.Server 24 | exit chan struct{} 25 | } 26 | 27 | func (a *Agent) Start(s service.Service) error { 28 | if service.Interactive() { 29 | logger.Info("Running in terminal.") 30 | } else { 31 | logger.Info("Running under service manager.") 32 | } 33 | a.exit = make(chan struct{}) 34 | 35 | go a.Run() 36 | return nil 37 | } 38 | 39 | func (a *Agent) Run() { 40 | if isFirstRun { 41 | isFirstRun = false 42 | 43 | logger.Info("Started HTTP Server.") 44 | 45 | r := gin.Default() 46 | key, err := getKey() 47 | if err != nil { 48 | logger.Error(err.Error()) 49 | } 50 | 51 | rg := restgate.New("X-Auth-Key", "", restgate.Static, restgate.Config{ 52 | Key: []string{key}, 53 | Debug: true, 54 | HTTPSProtectionOff: true, 55 | }) 56 | 57 | rgAdapter := func(c *gin.Context) { 58 | nextCalled := false 59 | nextAdapter := func(http.ResponseWriter, *http.Request) { 60 | nextCalled = true 61 | c.Next() 62 | } 63 | rg.ServeHTTP(c.Writer, c.Request, nextAdapter) 64 | if !nextCalled { 65 | c.AbortWithStatus(401) 66 | } 67 | } 68 | 69 | r.Use(rgAdapter) 70 | 71 | r.POST("/ping", func(c *gin.Context) { 72 | c.Header("X-WinAFLPet-Ver", fmt.Sprintf("%s (rev %s)", BuildVer, BuildRev)) 73 | c.Data(http.StatusOK, "text/plain", []byte("pong")) 74 | }) 75 | 76 | r.POST("/job/:guid/:action", func(c *gin.Context) { 77 | switch action := c.Param("action"); action { 78 | case "start": 79 | startJob(c) 80 | case "stop": 81 | stopJob(c) 82 | case "view": 83 | viewJob(c) 84 | case "check": 85 | checkJob(c) 86 | case "collect": 87 | collectJob(c) 88 | case "plot": 89 | plotJob(c) 90 | default: 91 | c.JSON(http.StatusNotImplemented, gin.H{}) 92 | } 93 | }) 94 | 95 | r.POST("/crash/:guid/:action", func(c *gin.Context) { 96 | switch action := c.Param("action"); action { 97 | case "verify": 98 | verifyCrash(c) 99 | case "download": 100 | downloadCrash(c) 101 | default: 102 | c.JSON(http.StatusNotImplemented, gin.H{}) 103 | } 104 | }) 105 | 106 | a.Server = http.Server{ 107 | Addr: ":8080", 108 | Handler: r, 109 | } 110 | 111 | go func() { 112 | if err := a.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 113 | logger.Errorf("Server was unable to start: %s", err) 114 | } 115 | }() 116 | } 117 | } 118 | func (a *Agent) Stop(s service.Service) error { 119 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 120 | defer cancel() 121 | if err := a.Server.Shutdown(ctx); err != nil { 122 | logger.Errorf("Server was forced to shutdown: %s", err) 123 | } 124 | 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /agent/crash.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package main 5 | 6 | import ( 7 | "bufio" 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | "os" 12 | "os/exec" 13 | "path" 14 | "path/filepath" 15 | "regexp" 16 | "strings" 17 | "syscall" 18 | 19 | "github.com/gin-gonic/gin" 20 | "github.com/rs/xid" 21 | ) 22 | 23 | const BUGID_BUG_NOT_DETECTED = "The application terminated without a bug being detected" 24 | 25 | type Crash struct { 26 | JobGUID xid.ID `json:"jguid"` 27 | FuzzerID string `json:"fuzzerid"` 28 | BugID string `json:"bugid"` 29 | Module string `json:"mod"` 30 | Function string `json:"func"` 31 | Description string `json:"desc"` 32 | Impact string `json:"imp"` 33 | Args string `json:"args"` 34 | } 35 | 36 | func newCrash(jobGUID xid.ID, fuzzerID string, args string) Crash { 37 | c := new(Crash) 38 | c.JobGUID = jobGUID 39 | c.FuzzerID = fuzzerID 40 | c.Args = args 41 | return *c 42 | } 43 | 44 | func (c Crash) Verify() (Crash, error) { 45 | GUID := c.JobGUID.String() 46 | 47 | job, _, err := project.GetJob(GUID) 48 | if err != nil { 49 | logger.Error(err) 50 | return c, err 51 | } 52 | 53 | python, err := exec.LookPath(path.Join(job.PyDir, "python.exe")) 54 | if err != nil { 55 | logger.Error(err) 56 | return c, err 57 | } 58 | 59 | bugid, err := exec.LookPath(path.Join(job.BugIdDir, "BugId.cmd")) 60 | if err != nil { 61 | logger.Error(err) 62 | return c, err 63 | } 64 | 65 | targetCmd, targetArgs := splitCmdLine(job.TargetApp) 66 | 67 | args := fmt.Sprintf("-q"+ 68 | " --bShowLicenseAndDonationInfo=false"+ 69 | " --bGenerateReportHTML=false"+ 70 | " --cBugId.bEnsurePageHeap=false"+ 71 | " --isa=%s"+ 72 | " %s --"+ 73 | " %s"+ 74 | " -f %s", // Sample delivery via file. 75 | job.TargetArch, targetCmd, targetArgs, c.Args) 76 | cmd := exec.Command(bugid, args) 77 | cmd.Dir = job.BugIdDir 78 | cmd.Env = append( 79 | os.Environ(), 80 | fmt.Sprintf("PYTHON=%s", python), 81 | ) 82 | cmd.SysProcAttr = &syscall.SysProcAttr{} 83 | cmd.SysProcAttr.CmdLine = strings.Join(cmd.Args, ` `) 84 | stdout, _ := cmd.StdoutPipe() 85 | if err := cmd.Start(); err != nil { 86 | logger.Error(err) 87 | return c, err 88 | } 89 | 90 | if cmd.Process != nil { 91 | buf := bufio.NewReader(stdout) 92 | for { 93 | line, _, _ := buf.ReadLine() 94 | s := string(line) 95 | if strings.Contains(s, BUGID_BUG_NOT_DETECTED) { 96 | return c, errors.New("no bug detected") 97 | } 98 | if m := regexp.MustCompile(`Id @ Location: +(.*) @ (.*)`).FindStringSubmatch(s); len(m) > 0 { 99 | c.BugID = m[1] 100 | re := regexp.MustCompile(`[!+]`) 101 | c.Module = re.Split(m[2], -1)[1] 102 | c.Function = re.Split(m[2], -1)[2] 103 | } 104 | if m := regexp.MustCompile(`Description: +(.*)`).FindStringSubmatch(s); len(m) > 0 { 105 | c.Description = m[1] 106 | } 107 | if m := regexp.MustCompile(`Security impact: +(.*)`).FindStringSubmatch(s); len(m) > 0 { 108 | c.Impact = m[1] 109 | return c, nil 110 | } 111 | } 112 | } 113 | 114 | return c, nil 115 | } 116 | 117 | func verifyCrash(c *gin.Context) { 118 | var crash Crash 119 | if err := c.ShouldBindJSON(&crash); err != nil { 120 | c.JSON(http.StatusBadRequest, gin.H{ 121 | "error": err.Error(), 122 | }) 123 | return 124 | } 125 | 126 | crash, err := crash.Verify() 127 | if err != nil { 128 | c.JSON(http.StatusInternalServerError, gin.H{ 129 | "error": err.Error(), 130 | }) 131 | return 132 | } 133 | 134 | c.JSON(http.StatusOK, crash) 135 | } 136 | 137 | func downloadCrash(c *gin.Context) { 138 | var crash Crash 139 | if err := c.ShouldBindJSON(&crash); err != nil { 140 | c.JSON(http.StatusBadRequest, gin.H{ 141 | "error": err.Error(), 142 | }) 143 | return 144 | } 145 | 146 | filePath := crash.Args 147 | if !fileExists(filePath) { 148 | c.JSON(http.StatusInternalServerError, gin.H{ 149 | "error": "The provided file path is invalid.", 150 | }) 151 | return 152 | } 153 | 154 | c.Header("Content-Transfer-Encoding", "binary") 155 | c.Header("Content-Disposition", "attachment; filename="+filepath.Base(filePath)) 156 | c.Header("Content-Type", "application/octet-stream") 157 | 158 | c.File(filePath) 159 | } 160 | -------------------------------------------------------------------------------- /agent/go.mod: -------------------------------------------------------------------------------- 1 | module agent 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/danieljoos/wincred v1.1.2 9 | github.com/gin-gonic/gin v1.9.1 10 | github.com/kardianos/service v1.2.0 11 | github.com/karrick/godirwalk v1.16.1 12 | github.com/mitchellh/go-ps v1.0.0 13 | github.com/pjebs/restgate v0.0.0-20200504001537-fd9a58a4fe75 14 | github.com/rs/xid v1.3.0 15 | github.com/spf13/pflag v1.0.5 16 | golang.org/x/crypto v0.36.0 17 | ) 18 | 19 | require ( 20 | github.com/bytedance/sonic v1.9.1 // indirect 21 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 22 | github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 // indirect 23 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 24 | github.com/gin-contrib/sse v0.1.0 // indirect 25 | github.com/go-playground/locales v0.14.1 // indirect 26 | github.com/go-playground/universal-translator v0.18.1 // indirect 27 | github.com/go-playground/validator/v10 v10.14.0 // indirect 28 | github.com/goccy/go-json v0.10.2 // indirect 29 | github.com/google/go-cmp v0.6.0 // indirect 30 | github.com/json-iterator/go v1.1.12 // indirect 31 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 32 | github.com/kr/pretty v0.3.0 // indirect 33 | github.com/leodido/go-urn v1.2.4 // indirect 34 | github.com/mattn/go-isatty v0.0.19 // indirect 35 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 36 | github.com/modern-go/reflect2 v1.0.2 // indirect 37 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 38 | github.com/pjebs/jsonerror v0.0.0-20190614034432-63ef9a8df848 // indirect 39 | github.com/rogpeppe/go-internal v1.8.0 // indirect 40 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 41 | github.com/ugorji/go/codec v1.2.11 // indirect 42 | golang.org/x/arch v0.3.0 // indirect 43 | golang.org/x/net v0.38.0 // indirect 44 | golang.org/x/sys v0.31.0 // indirect 45 | golang.org/x/term v0.30.0 // indirect 46 | golang.org/x/text v0.23.0 // indirect 47 | google.golang.org/protobuf v1.33.0 // indirect 48 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 49 | gopkg.in/unrolled/render.v1 v1.0.0 // indirect 50 | gopkg.in/yaml.v3 v3.0.1 // indirect 51 | ) 52 | -------------------------------------------------------------------------------- /agent/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 2 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= 3 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 4 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 5 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 6 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 7 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 8 | github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= 9 | github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o= 14 | github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= 15 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 16 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 17 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 18 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 19 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 20 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 21 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 22 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 23 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 24 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 25 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 26 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 27 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= 28 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 29 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 30 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 31 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 32 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 33 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 34 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 35 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 36 | github.com/kardianos/service v1.2.0 h1:bGuZ/epo3vrt8IPC7mnKQolqFeYJb7Cs8Rk4PSOBB/g= 37 | github.com/kardianos/service v1.2.0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= 38 | github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= 39 | github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= 40 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 41 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= 42 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 43 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 44 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 45 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 46 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 47 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 48 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 49 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 50 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 51 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 52 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 53 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 54 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 55 | github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= 56 | github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= 57 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 58 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 59 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 60 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 61 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 62 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= 63 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 64 | github.com/pjebs/jsonerror v0.0.0-20190614034432-63ef9a8df848 h1:XowRe4lNFAvUyqrGWsw5iXgSIAB88dvwkhG6Y358Mpk= 65 | github.com/pjebs/jsonerror v0.0.0-20190614034432-63ef9a8df848/go.mod h1:aTxn8DgzMXFgHW45SD0fKjhF3+vSL5Adh0+vhWmVH4g= 66 | github.com/pjebs/restgate v0.0.0-20200504001537-fd9a58a4fe75 h1:3LrfUJEfIJKaaTMDTcDflGH73pfr2ZD8flVYPykmwrY= 67 | github.com/pjebs/restgate v0.0.0-20200504001537-fd9a58a4fe75/go.mod h1:bkPh45UuaiyL0M8J/xecd9A5kzg/FqODoQgLP0SWC5s= 68 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 69 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 70 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 71 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 72 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 73 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 74 | github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= 75 | github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 76 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 77 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 78 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 79 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 80 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 81 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 82 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 83 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 84 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 85 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 86 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 87 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 88 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 89 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 90 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 91 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 92 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 93 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 94 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 95 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= 96 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 97 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 98 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 99 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 100 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 101 | golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 102 | golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 103 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 104 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 105 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 106 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 107 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 108 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 109 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 110 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 111 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 112 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 113 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 114 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 115 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 116 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 117 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 118 | gopkg.in/unrolled/render.v1 v1.0.0 h1:f/6S5YVJqWYcqYtvfsv+Eb+A1CWhuA7n+ILks5qv9gw= 119 | gopkg.in/unrolled/render.v1 v1.0.0/go.mod h1:D8ZfMFuggVdNUNlNz/R8zVjPPHGyMxLuJPA+MSx8na0= 120 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 121 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 122 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 123 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 124 | -------------------------------------------------------------------------------- /agent/job.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package main 5 | 6 | import ( 7 | "bufio" 8 | "errors" 9 | "fmt" 10 | "math/rand" 11 | "net/http" 12 | "os" 13 | "os/exec" 14 | "path" 15 | "path/filepath" 16 | "regexp" 17 | "strconv" 18 | "strings" 19 | "syscall" 20 | "time" 21 | 22 | "github.com/gin-gonic/gin" 23 | "github.com/karrick/godirwalk" 24 | "github.com/mitchellh/go-ps" 25 | "github.com/rs/xid" 26 | ) 27 | 28 | const ( 29 | AFL_EXECUTABLE = "afl-fuzz.exe" 30 | AFL_SUCCESS_MSG = "All set and ready to roll!" 31 | AFL_FAIL_REGEX = `(?:PROGRAM ABORT|OS message) : (.*)` 32 | AFL_STATS_FILE = "fuzzer_stats" 33 | AFL_PLOT_FILE = "plot_data" 34 | ) 35 | 36 | type Job struct { 37 | GUID xid.ID `json:"guid"` 38 | Name string `json:"name"` 39 | Description string `json:"desc"` 40 | Banner string `json:"banner"` 41 | Cores int `json:"cores"` 42 | Input string `json:"input"` 43 | Output string `json:"output"` 44 | Timeout int `json:"timeout"` 45 | InstMode string `json:"inst_mode"` 46 | DelivMode string `json:"deliv_mode"` 47 | CoverageType string `json:"cov_type"` 48 | CoverageModule string `json:"cov_module"` 49 | FuzzIter int `json:"fuzz_iter"` 50 | TargetModule string `json:"target_module"` 51 | TargetMethod string `json:"target_method"` 52 | TargetOffset string `json:"target_offset"` 53 | TargetNArgs int `json:"target_nargs"` 54 | TargetApp string `json:"target_app"` 55 | TargetArch string `json:"target_arch"` 56 | AFLDir string `json:"afl_dir"` 57 | DrioDir string `json:"drio_dir"` 58 | PyDir string `json:"py_dir"` 59 | BugIdDir string `json:"bugid_dir"` 60 | ExtrasDir string `json:"extras_dir"` 61 | AttachLib string `json:"attach_lib"` 62 | CustomLib string `json:"custom_lib"` 63 | MemoryLimit string `json:"memory_limit"` 64 | PersistCache int `json:"persist_cache"` 65 | DirtyMode int `json:"dirty_mode"` 66 | DumbMode int `json:"dumb_mode"` 67 | CrashMode int `json:"crash_mode"` 68 | BugBucket int `json:"bug_bucket"` 69 | ExpertMode int `json:"expert_mode"` 70 | VariableMode int `json:"variable_mode"` 71 | SequentialMode int `json:"sequential_mode"` 72 | NoAffinity int `json:"no_affinity"` 73 | SkipCrashes int `json:"skip_crashes"` 74 | ShuffleQueue int `json:"shuffle_queue"` 75 | Autoresume int `json:"autoresume"` 76 | Status int `json:"status"` 77 | } 78 | 79 | func newJob(GUID string) Job { 80 | j := new(Job) 81 | j.Cores = 1 82 | j.GUID, _ = xid.FromString(GUID) 83 | return *j 84 | } 85 | 86 | func (j Job) Start(fID int) error { 87 | binDir := "bin32" 88 | if j.TargetArch == "x64" { 89 | binDir = "bin64" 90 | } 91 | 92 | j.AFLDir = path.Join(j.AFLDir, binDir) 93 | j.DrioDir = path.Join(j.DrioDir, binDir) 94 | 95 | if j.VariableMode != 0 { 96 | j.CoverageType = []string{"edge", "bb"}[rand.Intn(2)] 97 | j.FuzzIter = int(float64(j.FuzzIter) * (1 + (rand.Float64() - 0.5))) 98 | } 99 | 100 | afl, err := exec.LookPath(path.Join(j.AFLDir, AFL_EXECUTABLE)) 101 | if err != nil { 102 | logger.Error(err) 103 | return err 104 | } 105 | 106 | targetCmd, targetArgs := splitCmdLine(j.TargetApp) 107 | if j.SequentialMode != 0 { 108 | targetCmd = sequentialName(targetCmd, fID) 109 | j.TargetModule = sequentialName(j.TargetModule, fID) 110 | } 111 | 112 | targetApp, err := exec.LookPath(targetCmd) 113 | if err != nil { 114 | logger.Error(err) 115 | return err 116 | } 117 | 118 | envs := os.Environ() 119 | 120 | if j.Autoresume != 0 { 121 | envs = append(envs, "AFL_AUTORESUME=1") 122 | } 123 | 124 | if j.SkipCrashes != 0 || j.Autoresume != 0 { 125 | envs = append(envs, "AFL_SKIP_CRASHES=1") 126 | } 127 | 128 | if j.ShuffleQueue != 0 { 129 | envs = append(envs, "AFL_SHUFFLE_QUEUE=1") 130 | } 131 | 132 | if j.NoAffinity != 0 { 133 | envs = append(envs, "AFL_NO_AFFINITY=1") 134 | } 135 | 136 | args := []string{} 137 | 138 | if j.DelivMode == "sm" { 139 | args = append(args, "-s") 140 | targetArgs += "-s @@" 141 | } else { 142 | targetArgs += "-f @@" 143 | } 144 | 145 | opRole := "-S" 146 | fuzzerID := fmt.Sprintf("%s%d", j.Banner, fID) 147 | if j.Cores > 1 && fID == 1 { 148 | opRole = "-M" 149 | } 150 | 151 | args = append(args, fmt.Sprintf("%s %s", opRole, fuzzerID)) 152 | args = append(args, fmt.Sprintf("-i %s", j.Input)) 153 | args = append(args, fmt.Sprintf("-o %s", j.Output)) 154 | args = append(args, fmt.Sprintf("-D %s", j.DrioDir)) 155 | 156 | timeoutSuffix := "" 157 | if j.Autoresume != 0 || j.Input == "-" { 158 | timeoutSuffix = "+" // Skip queue entries that time out. 159 | } 160 | 161 | args = append(args, fmt.Sprintf("-t %d%s", j.Timeout, timeoutSuffix)) 162 | 163 | if j.PersistCache != 0 { 164 | args = append(args, "-p") 165 | } 166 | 167 | if j.DirtyMode != 0 { 168 | args = append(args, "-d") 169 | } 170 | 171 | if j.BugBucket != 0 { 172 | args = append(args, "-b") 173 | } 174 | 175 | if j.ExpertMode != 0 { 176 | args = append(args, "-e") 177 | } 178 | 179 | if j.CrashMode != 0 && j.DumbMode == 0 { 180 | args = append(args, "-C") 181 | } 182 | 183 | if j.DumbMode != 0 && j.CrashMode == 0 { 184 | args = append(args, "-n") 185 | } 186 | 187 | if j.MemoryLimit != "0" && j.MemoryLimit != "" { 188 | args = append(args, fmt.Sprintf("-m %s", j.MemoryLimit)) 189 | } 190 | 191 | if j.AttachLib != "" { 192 | args = append(args, fmt.Sprintf("-A %s", j.AttachLib)) 193 | } 194 | 195 | if j.CustomLib != "" { 196 | args = append(args, fmt.Sprintf("-l %s", j.CustomLib)) 197 | } 198 | 199 | if j.ExtrasDir != "" { 200 | args = append(args, fmt.Sprintf("-x %s", j.ExtrasDir)) 201 | } 202 | 203 | args = append(args, "--") 204 | args = append(args, fmt.Sprintf("-covtype %s", j.CoverageType)) 205 | 206 | for _, m := range strings.Split(j.CoverageModule, ",") { 207 | args = append(args, fmt.Sprintf("-coverage_module %s", m)) 208 | } 209 | 210 | args = append(args, fmt.Sprintf("-fuzz_iterations %d", j.FuzzIter)) 211 | args = append(args, fmt.Sprintf("-target_module %s", j.TargetModule)) 212 | args = append(args, fmt.Sprintf("-target_method %s", j.TargetMethod)) 213 | args = append(args, fmt.Sprintf("-target_offset %s", j.TargetOffset)) 214 | args = append(args, fmt.Sprintf("-nargs %d", j.TargetNArgs)) 215 | args = append(args, "--") 216 | args = append(args, targetApp) 217 | args = append(args, targetArgs) 218 | 219 | cmd := exec.Command(afl, strings.Join(args, " ")) 220 | cmd.Dir = j.AFLDir 221 | cmd.Env = envs 222 | cmd.SysProcAttr = &syscall.SysProcAttr{} 223 | cmd.SysProcAttr.CmdLine = strings.Join(cmd.Args, ` `) 224 | stdoutPipe, _ := cmd.StdoutPipe() 225 | stdoutReader := bufio.NewReader(stdoutPipe) 226 | 227 | if err := cmd.Start(); err != nil { 228 | logger.Error(err) 229 | return err 230 | } 231 | 232 | c := make(chan error) 233 | 234 | go readStdout(c, stdoutReader) 235 | 236 | select { 237 | case err := <-c: 238 | return err 239 | case <-time.After(4 * time.Minute): 240 | return nil 241 | } 242 | } 243 | 244 | func (j Job) Stop() error { 245 | processes, err := ps.Processes() 246 | if err != nil { 247 | logger.Error(err) 248 | return err 249 | } 250 | 251 | targetCmd, _ := splitCmdLine(j.TargetApp) 252 | targetExe := filepath.Base(targetCmd) 253 | targetProcs := []ps.Process{} 254 | 255 | i := strings.LastIndex(targetExe, ".exe") 256 | expr := fmt.Sprintf("^%s\\d*%s$", targetExe[:i], targetExe[i:]) 257 | re, _ := regexp.Compile(expr) 258 | 259 | for _, p := range processes { 260 | if re.MatchString(p.Executable()) { 261 | p1, _ := ps.FindProcess(p.Pid()) 262 | if p1 != nil { 263 | targetProcs = append(targetProcs, p1) 264 | p2, _ := ps.FindProcess(p1.PPid()) 265 | if p2 != nil { 266 | targetProcs = append(targetProcs, p2) 267 | p3, _ := ps.FindProcess(p2.PPid()) 268 | if p3 != nil { 269 | targetProcs = append(targetProcs, p3) 270 | } 271 | } 272 | } 273 | } 274 | } 275 | 276 | for _, p := range targetProcs { 277 | killProcess(p) 278 | } 279 | 280 | return nil 281 | } 282 | 283 | func (j Job) View() ([]Stats, error) { 284 | var stats []Stats 285 | 286 | for c := 1; c <= j.Cores; c++ { 287 | fuzzerID := fmt.Sprintf("%s%d", j.Banner, c) 288 | fileName := joinPath(j.AFLDir, j.Output, fuzzerID, AFL_STATS_FILE) 289 | 290 | if !fileExists(fileName) { 291 | text := fmt.Sprintf("Statistics are unavailable for fuzzer instance #%d in job %s", c, j.Name) 292 | err := errors.New(text) 293 | return stats, err 294 | } 295 | 296 | content, err := os.ReadFile(fileName) 297 | if err != nil { 298 | continue 299 | } 300 | 301 | newStats, err := parseStats(string(content)) 302 | if err != nil { 303 | continue 304 | } 305 | 306 | stats = append(stats, newStats) 307 | } 308 | 309 | return stats, nil 310 | } 311 | 312 | func (j Job) Check(pid int) (bool, error) { 313 | p, err := ps.FindProcess(pid) 314 | if err != nil { 315 | return false, err 316 | } 317 | 318 | if p != nil && strings.Contains(p.Executable(), "afl-fuzz.exe") { 319 | return true, nil 320 | } 321 | 322 | return false, nil 323 | } 324 | 325 | func (j Job) Collect() ([]Crash, error) { 326 | var crashes []Crash 327 | 328 | dirname := joinPath(j.AFLDir, j.Output) 329 | re := regexp.MustCompile(`\\crashes(_\d{14})?\\id_\d{6}_\w+$`) 330 | err := godirwalk.Walk(dirname, &godirwalk.Options{ 331 | Callback: func(osPathname string, de *godirwalk.Dirent) error { 332 | if re.MatchString(osPathname) { 333 | crashDir := strings.Split(filepath.Dir(osPathname), "\\") 334 | fuzzerID := crashDir[len(crashDir)-2] 335 | newCrash := newCrash(j.GUID, fuzzerID, osPathname) 336 | crashes = append(crashes, newCrash) 337 | } 338 | return nil 339 | }, 340 | Unsorted: true, 341 | }) 342 | 343 | return crashes, err 344 | } 345 | 346 | func startJob(c *gin.Context) { 347 | j := newJob(c.Param("guid")) 348 | 349 | fID, err := strconv.Atoi(c.DefaultQuery("fid", "1")) 350 | if err != nil { 351 | c.JSON(http.StatusBadRequest, gin.H{ 352 | "guid": c.Param("guid"), 353 | "error": err.Error(), 354 | }) 355 | return 356 | } 357 | 358 | if err := c.ShouldBindJSON(&j); err != nil { 359 | c.JSON(http.StatusBadRequest, gin.H{ 360 | "guid": c.Param("guid"), 361 | "error": err.Error(), 362 | }) 363 | return 364 | } 365 | 366 | if err := j.Start(fID); err != nil { 367 | c.JSON(http.StatusInternalServerError, gin.H{ 368 | "guid": j.GUID, 369 | "error": err.Error(), 370 | }) 371 | return 372 | } 373 | 374 | if ok, _ := project.FindJob(j.GUID); !ok { 375 | project.AddJob(j) 376 | } 377 | 378 | c.JSON(http.StatusCreated, gin.H{ 379 | "guid": j.GUID, 380 | "msg": fmt.Sprintf("Fuzzer instance #%d of job %s has been successfuly started!", fID, j.Name), 381 | }) 382 | } 383 | 384 | func stopJob(c *gin.Context) { 385 | j, i, err := project.GetJob(c.Param("guid")) 386 | if err != nil { 387 | c.JSON(http.StatusNotFound, gin.H{ 388 | "guid": c.Param("guid"), 389 | "error": err.Error(), 390 | }) 391 | return 392 | } 393 | 394 | if err := j.Stop(); err != nil { 395 | c.JSON(http.StatusInternalServerError, gin.H{ 396 | "guid": j.GUID, 397 | "error": err.Error(), 398 | }) 399 | return 400 | } 401 | 402 | project.RemoveJob(i) 403 | 404 | c.JSON(http.StatusOK, gin.H{ 405 | "guid": j.GUID, 406 | "msg": fmt.Sprintf("Job %s has been successfully stopped!", j.Name), 407 | }) 408 | } 409 | 410 | func viewJob(c *gin.Context) { 411 | j, _, err := project.GetJob(c.Param("guid")) 412 | if err != nil { 413 | c.JSON(http.StatusNotFound, gin.H{ 414 | "guid": c.Param("guid"), 415 | "error": err.Error(), 416 | }) 417 | return 418 | } 419 | 420 | Stats, err := j.View() 421 | if err != nil { 422 | c.JSON(http.StatusNotFound, gin.H{ 423 | "guid": j.GUID, 424 | "error": err.Error(), 425 | }) 426 | return 427 | } 428 | 429 | c.JSON(http.StatusOK, Stats) 430 | } 431 | 432 | func checkJob(c *gin.Context) { 433 | processIDs := []int{} 434 | msg := "" 435 | 436 | c.Bind(&processIDs) 437 | if len(processIDs) < 1 || len(processIDs) > 40 { 438 | c.JSON(http.StatusInternalServerError, gin.H{ 439 | "error": "Invalid number of arguments provided.", 440 | }) 441 | return 442 | } 443 | 444 | j, _, err := project.GetJob(c.Param("guid")) 445 | if err != nil { 446 | c.JSON(http.StatusNotFound, gin.H{ 447 | "error": err.Error(), 448 | }) 449 | return 450 | } 451 | 452 | for _, processID := range processIDs { 453 | ok, err := j.Check(processID) 454 | if err != nil { 455 | c.JSON(http.StatusInternalServerError, gin.H{ 456 | "error": err.Error(), 457 | }) 458 | return 459 | } 460 | if !ok { 461 | c.JSON(http.StatusNotFound, gin.H{ 462 | "msg": fmt.Sprintf("Fuzzer instance with PID %d cannot be found.", processID), 463 | "pid": processID, 464 | }) 465 | return 466 | } 467 | } 468 | 469 | if len(processIDs) > 1 { 470 | msg = "All fuzzer instances seem to be up and running." 471 | } else { 472 | msg = fmt.Sprintf("Fuzzer instance with PID %d is up and running.", processIDs[0]) 473 | } 474 | 475 | c.JSON(http.StatusOK, gin.H{ 476 | "msg": msg, 477 | }) 478 | } 479 | 480 | func collectJob(c *gin.Context) { 481 | j, _, err := project.GetJob(c.Param("guid")) 482 | if err != nil { 483 | c.JSON(http.StatusNotFound, gin.H{ 484 | "guid": c.Param("guid"), 485 | "error": err.Error(), 486 | }) 487 | return 488 | } 489 | 490 | Crashes, err := j.Collect() 491 | if err != nil { 492 | c.JSON(http.StatusNotFound, gin.H{ 493 | "guid": j.GUID, 494 | "error": err.Error(), 495 | }) 496 | return 497 | } 498 | 499 | c.JSON(http.StatusOK, Crashes) 500 | } 501 | 502 | func plotJob(c *gin.Context) { 503 | j, _, err := project.GetJob(c.Param("guid")) 504 | if err != nil { 505 | c.JSON(http.StatusNotFound, gin.H{ 506 | "guid": c.Param("guid"), 507 | "error": err.Error(), 508 | }) 509 | return 510 | } 511 | 512 | fID, err := strconv.Atoi(c.Query("fid")) 513 | if err != nil { 514 | c.JSON(http.StatusBadRequest, gin.H{ 515 | "guid": c.Param("guid"), 516 | "error": err.Error(), 517 | }) 518 | return 519 | } 520 | 521 | fuzzerID := fmt.Sprintf("%s%d", j.Banner, fID) 522 | filePath := joinPath(j.AFLDir, j.Output, fuzzerID, AFL_PLOT_FILE) 523 | // TODO: Add a security check for filepath. 524 | // if !strings.HasPrefix(filepath.Clean(filePath), "C:\\Tools\\") { 525 | // c.String(403, "Invalid file path!") 526 | // return 527 | // } 528 | 529 | c.Header("Content-Transfer-Encoding", "binary") 530 | c.Header("Content-Disposition", "attachment; filename="+AFL_PLOT_FILE) 531 | c.Header("Content-Type", "application/octet-stream") 532 | c.File(filePath) 533 | } 534 | -------------------------------------------------------------------------------- /agent/main.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "os" 10 | "syscall" 11 | 12 | flag "github.com/spf13/pflag" 13 | 14 | "github.com/kardianos/service" 15 | "golang.org/x/crypto/ssh/terminal" 16 | ) 17 | 18 | var ( 19 | BuildVer string 20 | BuildRev string 21 | logger service.Logger 22 | ) 23 | 24 | func main() { 25 | var ( 26 | action string 27 | version bool 28 | username string 29 | ) 30 | 31 | flag.StringVarP(&action, "service", "s", "", "Control the system service.") 32 | flag.BoolVarP(&version, "version", "v", false, "Output the current version of the agent.") 33 | flag.Parse() 34 | 35 | if version { 36 | fmt.Printf("WinAFL Pet Agent v%s (rev %s)\n", BuildVer, BuildRev) 37 | os.Exit(0) 38 | } 39 | 40 | options := make(service.KeyValue) 41 | 42 | if action == "install" { 43 | fmt.Print("Username of service account: ") 44 | fmt.Scanln(&username) 45 | fmt.Print("Password of service account: ") 46 | password, _ := terminal.ReadPassword(int(syscall.Stdin)) 47 | options["Password"] = string(password) 48 | } 49 | 50 | svcName := "WinAFLPetAgent" 51 | svcConfig := &service.Config{ 52 | Name: svcName, 53 | DisplayName: "WinAFL Pet Agent", 54 | Description: "This is a service agent exposing an API to manage WinAFL.", 55 | UserName: username, 56 | Option: options, 57 | } 58 | 59 | a := &Agent{} 60 | 61 | s, err := service.New(a, svcConfig) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | 66 | errs := make(chan error, 5) 67 | logger, err = s.Logger(nil) 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | 72 | go func() { 73 | for { 74 | err := <-errs 75 | if err != nil { 76 | log.Print(err) 77 | } 78 | } 79 | }() 80 | 81 | if len(action) != 0 { 82 | err := service.Control(s, action) 83 | if err != nil { 84 | log.Printf("Valid actions: %q\n", service.ControlAction) 85 | log.Fatal(err) 86 | } 87 | 88 | switch action { 89 | case service.ControlAction[3]: 90 | if err := initKey(); err != nil { 91 | log.Fatal(err) 92 | } 93 | case service.ControlAction[4]: 94 | if err := delKey(); err != nil { 95 | log.Fatal(err) 96 | } 97 | default: 98 | // not required 99 | } 100 | 101 | return 102 | } 103 | 104 | err = s.Run() 105 | if err != nil { 106 | log.Fatal(err) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /agent/project.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package main 5 | 6 | import ( 7 | "errors" 8 | 9 | "github.com/rs/xid" 10 | ) 11 | 12 | type Project struct { 13 | Jobs []Job 14 | } 15 | 16 | func (p *Project) AddJob(j Job) []Job { 17 | p.Jobs = append(p.Jobs, j) 18 | return p.Jobs 19 | } 20 | 21 | func (p *Project) RemoveJob(i int) []Job { 22 | copy(p.Jobs[i:], p.Jobs[i+1:]) 23 | 24 | if len(p.Jobs) == 1 { 25 | p.Jobs = nil 26 | } else { 27 | p.Jobs[i] = p.Jobs[len(p.Jobs)-1] 28 | p.Jobs = p.Jobs[:len(p.Jobs)-1] 29 | } 30 | 31 | return p.Jobs 32 | } 33 | 34 | func (p *Project) GetJob(guid string) (Job, int, error) { 35 | var j Job 36 | 37 | GUID, err := xid.FromString(guid) 38 | if err != nil { 39 | return j, 0, nil 40 | } 41 | 42 | for index, j := range p.Jobs { 43 | if j.GUID == GUID { 44 | return j, index, nil 45 | } 46 | } 47 | 48 | return j, 0, errors.New("Job not found") 49 | } 50 | 51 | func (p *Project) FindJob(GUID xid.ID) (bool, error) { 52 | for _, j := range p.Jobs { 53 | if j.GUID == GUID { 54 | return true, nil 55 | } 56 | } 57 | 58 | return false, nil 59 | } 60 | -------------------------------------------------------------------------------- /agent/stats.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package main 5 | 6 | type Stats struct { 7 | StartTime int `json:"start_time"` 8 | LastUpdate int `json:"last_update"` 9 | FuzzerProcessID int `json:"fuzzer_pid"` 10 | CyclesDone int `json:"cycles_done"` 11 | ExecsDone int `json:"execs_done"` 12 | ExecsPerSec float64 `json:"execs_per_sec"` 13 | PathsTotal int `json:"paths_total"` 14 | PathsFavored int `json:"paths_favored"` 15 | PathsFound int `json:"paths_found"` 16 | PathsImported int `json:"paths_imported"` 17 | MaxDepth int `json:"max_depth"` 18 | CurPath int `json:"cur_path"` 19 | PendingFavs int `json:"pending_favs"` 20 | PendingTotal int `json:"pending_total"` 21 | VariablePaths int `json:"variable_paths"` 22 | Stability string `json:"stability"` 23 | BitmapCvg string `json:"bitmap_cvg"` 24 | UniqueCrashes int `json:"unique_crashes"` 25 | UniqueHangs int `json:"unique_hangs"` 26 | LastPath int `json:"last_path"` 27 | LastCrash int `json:"last_crash"` 28 | LastHang int `json:"last_hang"` 29 | ExecsSinceCrash int `json:"execs_since_crash"` 30 | ExecTimeout int `json:"exec_timeout"` 31 | AFLBanner string `json:"afl_banner"` 32 | AFLVersion string `json:"afl_version"` 33 | } 34 | -------------------------------------------------------------------------------- /agent/utils.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package main 5 | 6 | import ( 7 | "bufio" 8 | "crypto/rand" 9 | "encoding/hex" 10 | "encoding/json" 11 | "errors" 12 | "fmt" 13 | "io" 14 | "os" 15 | "path/filepath" 16 | "regexp" 17 | "strconv" 18 | "strings" 19 | 20 | "github.com/danieljoos/wincred" 21 | "github.com/mitchellh/go-ps" 22 | ) 23 | 24 | const ( 25 | WINCRED_NAME = "WinAFL_Pet_Agent" 26 | ) 27 | 28 | func fileExists(filename string) bool { 29 | info, err := os.Stat(filename) 30 | if os.IsNotExist(err) { 31 | return false 32 | } 33 | return !info.IsDir() 34 | } 35 | 36 | func stripAnsi(s string) string { 37 | ansi := "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" 38 | re := regexp.MustCompile(ansi) 39 | return re.ReplaceAllString(s, "") 40 | } 41 | 42 | func killProcess(p ps.Process) error { 43 | proc, err := os.FindProcess(p.Pid()) 44 | if err != nil { 45 | logger.Error(err) 46 | return err 47 | } 48 | 49 | err = proc.Kill() 50 | if err != nil { 51 | logger.Error(err) 52 | return err 53 | } 54 | 55 | logger.Infof("Killed %s process (PID %d, PPID %d)\n", p.Executable(), p.Pid(), p.PPid()) 56 | 57 | return nil 58 | } 59 | 60 | func parseStats(content string) (Stats, error) { 61 | var stats Stats 62 | var fields = make(map[string]interface{}) 63 | 64 | lines := strings.Split(content, "\n") 65 | for _, line := range lines { 66 | if len(line) == 0 { 67 | break 68 | } 69 | 70 | s := strings.Split(line, ":") 71 | name := strings.TrimSpace(s[0]) 72 | value := strings.Replace(strings.TrimSpace(s[1]), "inf", "0.0", 1) 73 | fields[name] = value 74 | 75 | if strings.Contains(value, ".") { 76 | if f, err := strconv.ParseFloat(value, 64); err == nil { 77 | fields[name] = f 78 | } 79 | } else if i, err := strconv.Atoi(value); err == nil { 80 | fields[name] = i 81 | } 82 | } 83 | 84 | b, err := json.Marshal(fields) 85 | if err != nil { 86 | logger.Error(err) 87 | return stats, err 88 | } 89 | 90 | if err := json.Unmarshal([]byte(b), &stats); err != nil { 91 | logger.Error(err) 92 | return stats, err 93 | } 94 | 95 | return stats, nil 96 | } 97 | 98 | func genKey() string { 99 | b := make([]byte, 16) 100 | rand.Read(b) 101 | k := hex.EncodeToString(b) 102 | fmt.Println("\nSecret key of service account:", k) 103 | return k 104 | } 105 | 106 | func initKey() error { 107 | cred := wincred.NewGenericCredential(WINCRED_NAME) 108 | cred.CredentialBlob = []byte(genKey()) 109 | return cred.Write() 110 | } 111 | 112 | func getKey() (string, error) { 113 | cred, err := wincred.GetGenericCredential(WINCRED_NAME) 114 | if err != nil { 115 | return "", err 116 | } 117 | 118 | return string(cred.CredentialBlob), nil 119 | } 120 | 121 | func delKey() error { 122 | cred, err := wincred.GetGenericCredential(WINCRED_NAME) 123 | if err != nil { 124 | return err 125 | } 126 | return cred.Delete() 127 | } 128 | 129 | func splitCmdLine(cmdLine string) (string, string) { 130 | cmdFields := strings.Fields(cmdLine) 131 | 132 | cmd := cmdFields[0] 133 | args := "" 134 | 135 | if len(cmdFields) > 1 { 136 | args = strings.Join(cmdFields[1:], " ") 137 | } 138 | 139 | return cmd, args 140 | } 141 | 142 | func joinPath(workingDir string, outputDir string, pathNames ...string) string { 143 | e := append([]string{outputDir}, pathNames...) 144 | 145 | if !filepath.IsAbs(outputDir) { 146 | e = append([]string{workingDir}, e...) 147 | } 148 | 149 | p := filepath.Join(e...) 150 | 151 | return p 152 | } 153 | 154 | func readStdout(c chan error, rd *bufio.Reader) { 155 | for { 156 | l, _, err := rd.ReadLine() 157 | if err != nil || err == io.EOF { 158 | c <- err 159 | } 160 | 161 | s := string(l) 162 | if strings.Contains(s, AFL_SUCCESS_MSG) { 163 | c <- nil 164 | } 165 | 166 | m := regexp.MustCompile(AFL_FAIL_REGEX).FindStringSubmatch(s) 167 | if len(m) > 0 { 168 | c <- errors.New(stripAnsi(m[1])) 169 | } 170 | } 171 | } 172 | 173 | func sequentialName(name string, fID int) string { 174 | i := strings.LastIndex(name, ".exe") 175 | if i == -1 { 176 | return name 177 | } 178 | 179 | return fmt.Sprintf("%s%d%s", name[:i], fID, name[i:]) 180 | } 181 | -------------------------------------------------------------------------------- /hooks/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | docker build --build-arg BUILD_DATE=`git log --pretty=format:%ct -1` \ 3 | --build-arg BUILD_REV=`git rev-parse --short HEAD` \ 4 | --build-arg BUILD_VER=$SOURCE_BRANCH -t $IMAGE_NAME . 5 | -------------------------------------------------------------------------------- /server/agent.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/Masterminds/squirrel" 10 | "github.com/gin-gonic/gin" 11 | "github.com/parnurzeal/gorequest" 12 | "github.com/rs/xid" 13 | "github.com/sgabe/structable" 14 | ) 15 | 16 | const ( 17 | TB_NAME_AGENTS = "agents" 18 | TB_SCHEMA_AGENTS = `CREATE TABLE agents ( 19 | "id" INTEGER PRIMARY KEY AUTOINCREMENT, 20 | "guid" TEXT NOT NULL, 21 | "name" TEXT, 22 | "desc" TEXT, 23 | "host" TEXT NOT NULL, 24 | "port" INTEGER NOT NULL, 25 | "key" TEXT NOT NULL, 26 | "ver" TEXT, 27 | "status" INTEGER 28 | );` 29 | ) 30 | 31 | type Agent struct { 32 | structable.Recorder 33 | ID int `stbl:"id, PRIMARY_KEY, AUTO_INCREMENT"` 34 | GUID xid.ID `json:"guid" stbl:"guid"` 35 | Name string `json:"name" form:"name" stbl:"name"` 36 | Description string `json:"desc" form:"desc" stbl:"desc"` 37 | Host string `json:"host" form:"host" stbl:"host"` 38 | Port int `json:"port" form:"port" stbl:"port"` 39 | Key string `json:"key" form:"key" stbl:"key"` 40 | Version string `json:"ver" form:"ver" stbl:"ver"` 41 | Status int `json:"status" form:"status" stbl:"status"` 42 | } 43 | 44 | func newAgent() *Agent { 45 | a := new(Agent) 46 | a.GUID = xid.New() 47 | a.Status = 1 48 | a.Recorder = structable.New(db, DB_FLAVOR).Bind(TB_NAME_AGENTS, a) 49 | return a 50 | } 51 | 52 | func (a *Agent) loadByGUID() error { 53 | return a.Recorder.LoadWhere("guid = ?", a.GUID) 54 | } 55 | 56 | func loadAgents() ([]*Agent, error) { 57 | a := &Agent{} 58 | sa := structable.New(db, DB_FLAVOR).Bind(TB_NAME_AGENTS, a) 59 | 60 | fn := func(d structable.Describer, q squirrel.SelectBuilder) (squirrel.SelectBuilder, error) { 61 | return q.Limit(100), nil 62 | } 63 | 64 | items, err := listWhere(sa, fn) 65 | if err != nil { 66 | return []*Agent{}, err 67 | } 68 | 69 | // Because we get back a []Recorder, we need to get the original data 70 | // back out. We have to manually convert it back to its real type. 71 | agents := make([]*Agent, len(items)) 72 | for i, item := range items { 73 | agents[i] = item.Interface().(*Agent) 74 | } 75 | 76 | return agents, err 77 | } 78 | 79 | func createAgents(c *gin.Context) { 80 | switch c.Request.Method { 81 | case http.MethodGet: 82 | c.HTML(http.StatusOK, "agents_create", gin.H{ 83 | "title": "Create agent", 84 | }) 85 | return 86 | case http.MethodPut: 87 | a := newAgent() 88 | if err := c.ShouldBind(&a); err != nil { 89 | otherError(c, map[string]string{ 90 | "alert": err.Error(), 91 | }) 92 | return 93 | } 94 | if err := a.Insert(); err != nil { 95 | otherError(c, map[string]string{ 96 | "alert": err.Error(), 97 | }) 98 | return 99 | } 100 | c.JSON(http.StatusOK, gin.H{ 101 | "alert": fmt.Sprintf("Agent %s has been successfully created!", a.Name), 102 | "context": "success", 103 | }) 104 | default: 105 | c.JSON(http.StatusInternalServerError, gin.H{}) 106 | } 107 | } 108 | 109 | func viewAgents(c *gin.Context) { 110 | title := "Agents" 111 | 112 | agents, err := loadAgents() 113 | if err != nil { 114 | otherError(c, map[string]string{ 115 | "title": title, 116 | "alert": err.Error(), 117 | "template": "agents_view", 118 | }) 119 | return 120 | } 121 | 122 | c.HTML(http.StatusOK, "agents_view", gin.H{ 123 | "title": title, 124 | "agents": agents, 125 | }) 126 | } 127 | 128 | func deleteAgents(c *gin.Context) { 129 | agents := squirrel.Select("id").From(TB_NAME_AGENTS) 130 | rows, err := agents.RunWith(db).Query() 131 | if err != nil { 132 | otherError(c, map[string]string{"alert": err.Error()}) 133 | } 134 | 135 | defer rows.Close() 136 | 137 | for rows.Next() { 138 | a := newAgent() 139 | if err := rows.Scan(&a.ID); err != nil { 140 | otherError(c, map[string]string{"alert": err.Error()}) 141 | } 142 | if err := a.Load(); err != nil { 143 | otherError(c, map[string]string{"alert": err.Error()}) 144 | } 145 | a.Delete() 146 | } 147 | 148 | c.JSON(http.StatusOK, gin.H{ 149 | "alert": "All agents have been successfully deleted!", 150 | "context": "success", 151 | }) 152 | } 153 | 154 | func checkAgent(c *gin.Context) { 155 | a := newAgent() 156 | a.GUID, _ = xid.FromString(c.Param("guid")) 157 | if err := a.loadByGUID(); err != nil { 158 | otherError(c, map[string]string{"alert": err.Error()}) 159 | return 160 | } 161 | 162 | request := gorequest.New().Timeout(1000 * time.Millisecond) 163 | request.Debug = false 164 | 165 | targetURL := fmt.Sprintf("http://%s:%d/ping", a.Host, a.Port) 166 | resp, body, errs := request.Post(targetURL).Set("X-Auth-Key", a.Key).End() 167 | if errs != nil { 168 | otherError(c, map[string]string{"alert": errs[0].Error()}) 169 | return 170 | } 171 | 172 | if body != "pong" { 173 | otherError(c, map[string]string{"alert": body}) 174 | return 175 | } 176 | 177 | agentVersion := resp.Header.Get("X-WinAFLPet-Ver") 178 | if agentVersion != "" { 179 | a.Version = agentVersion 180 | a.Update() 181 | } 182 | 183 | c.JSON(http.StatusOK, gin.H{ 184 | "alert": fmt.Sprintf("Agent %s is up and running!", a.Name), 185 | "context": "success", 186 | }) 187 | } 188 | 189 | func deleteAgent(c *gin.Context) { 190 | a := newAgent() 191 | a.GUID, _ = xid.FromString(c.Param("guid")) 192 | if err := a.loadByGUID(); err != nil { 193 | otherError(c, map[string]string{"alert": err.Error()}) 194 | return 195 | } 196 | 197 | if err := a.Delete(); err != nil { 198 | otherError(c, map[string]string{"alert": err.Error()}) 199 | return 200 | } 201 | 202 | c.JSON(http.StatusOK, gin.H{ 203 | "alert": "Agent has been successfully deleted!", 204 | "context": "success", 205 | }) 206 | } 207 | 208 | func editAgent(c *gin.Context) { 209 | title := "Edit agent" 210 | 211 | a := newAgent() 212 | a.GUID, _ = xid.FromString(c.Param("guid")) 213 | 214 | switch c.Request.Method { 215 | case http.MethodGet: 216 | if err := a.loadByGUID(); err != nil { 217 | otherError(c, map[string]string{ 218 | "alert": err.Error(), 219 | "template": "agent_edit", 220 | }) 221 | return 222 | } 223 | c.HTML(http.StatusOK, "agent_edit", gin.H{ 224 | "title": title, 225 | "agent": a, 226 | }) 227 | case http.MethodPost: 228 | if err := a.loadByGUID(); err != nil { 229 | otherError(c, map[string]string{"alert": err.Error()}) 230 | return 231 | } 232 | if err := c.ShouldBind(&a); err != nil { 233 | otherError(c, map[string]string{ 234 | "title": title, 235 | "alert": err.Error(), 236 | "template": "agent_edit", 237 | }) 238 | return 239 | } 240 | a.Status, _ = strconv.Atoi(c.DefaultPostForm("status", "0")) 241 | if err := a.Update(); err != nil { 242 | otherError(c, map[string]string{ 243 | "title": title, 244 | "alert": err.Error(), 245 | "template": "agent_edit", 246 | }) 247 | return 248 | } 249 | c.Redirect(http.StatusFound, "/agents/view") 250 | default: 251 | c.JSON(http.StatusInternalServerError, gin.H{}) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /server/alert.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/mail" 9 | "net/smtp" 10 | "regexp" 11 | "strings" 12 | "time" 13 | 14 | "github.com/parnurzeal/gorequest" 15 | "github.com/rs/xid" 16 | "github.com/spf13/viper" 17 | ) 18 | 19 | type Alert struct { 20 | Jobs []Job 21 | } 22 | 23 | func (a *Alert) AddJob(j Job) []Job { 24 | a.Jobs = append(a.Jobs, j) 25 | return a.Jobs 26 | } 27 | 28 | func (a *Alert) RemoveJob(i int) []Job { 29 | copy(a.Jobs[i:], a.Jobs[i+1:]) 30 | 31 | if len(a.Jobs) == 1 { 32 | a.Jobs = nil 33 | } else { 34 | a.Jobs[i] = a.Jobs[len(a.Jobs)-1] 35 | a.Jobs = a.Jobs[:len(a.Jobs)-1] 36 | } 37 | 38 | return a.Jobs 39 | } 40 | 41 | func (a *Alert) GetJob(GUID xid.ID) (Job, int, error) { 42 | var j Job 43 | 44 | for index, j := range a.Jobs { 45 | if j.GUID == GUID { 46 | return j, index, nil 47 | } 48 | } 49 | 50 | return j, 0, errors.New("Job not found") 51 | } 52 | 53 | func (a *Alert) FindJob(GUID xid.ID) (bool, error) { 54 | for _, j := range a.Jobs { 55 | if j.GUID == GUID { 56 | return true, nil 57 | } 58 | } 59 | 60 | return false, nil 61 | } 62 | 63 | func (a *Alert) Monitor(j Job, m *mail.Address) { 64 | d := time.Duration(viper.GetInt("alert.interval")) * time.Minute 65 | ticker := time.NewTicker(d) 66 | 67 | for _ = range ticker.C { 68 | j.Recorder.Load() 69 | if j.Status == 0 { 70 | ticker.Stop() 71 | if _, i, err := a.GetJob(j.GUID); err == nil { 72 | a.RemoveJob(i) 73 | } 74 | return 75 | } 76 | 77 | request := gorequest.New() 78 | request.Debug = false 79 | 80 | agent, _ := j.GetAgent() 81 | targetURL := fmt.Sprintf("http://%s:%d/job/%s/collect", agent.Host, agent.Port, j.GUID) 82 | 83 | var crashesTemp []Crash 84 | resp, _, errs := request.Post(targetURL).Set("X-Auth-Key", agent.Key).EndStruct(&crashesTemp) 85 | if errs != nil || resp.StatusCode != http.StatusOK { 86 | ticker.Stop() 87 | if _, i, err := a.GetJob(j.GUID); err == nil { 88 | a.RemoveJob(i) 89 | } 90 | return 91 | } 92 | 93 | resumedJob := false 94 | if j.Input == "-" { 95 | resumedJob = true 96 | } 97 | 98 | var crashes []Crash 99 | for _, crash := range crashesTemp { 100 | c := newCrash() 101 | c.JobID = j.ID 102 | c.FuzzerID = crash.FuzzerID 103 | 104 | recentCrash := false 105 | for _, i := range crashesTemp { 106 | if i.FuzzerID == c.FuzzerID && strings.Contains(i.Args, "\\crashes\\") { 107 | recentCrash = true 108 | break 109 | } 110 | } 111 | 112 | re := regexp.MustCompile(c.FuzzerID + `\\crashes_\d{14}\\`) 113 | backedUpCrash := re.MatchString(crash.Args) 114 | 115 | // Avoid duplicate crash records when resuming aborted jobs. 116 | if resumedJob && !recentCrash && backedUpCrash { 117 | c.Args = re.ReplaceAllString(crash.Args, c.FuzzerID+"\\crashes\\") 118 | if err := c.LoadByJobIDArgs(); err == nil { 119 | c.Args = crash.Args 120 | if err := c.Update(); err != nil { 121 | log.Println(err) 122 | } 123 | continue 124 | } 125 | } 126 | 127 | c.Args = crash.Args 128 | if err := c.LoadByJobIDArgs(); err != nil { 129 | if err := c.Insert(); err != nil { 130 | log.Println(err) 131 | break 132 | } 133 | crashes = append(crashes, *c) 134 | } 135 | } 136 | 137 | if len(crashes) > 0 { 138 | host := viper.GetString("smtp.host") 139 | port := viper.GetInt("smtp.port") 140 | username := viper.GetString("smtp.username") 141 | password := viper.GetString("smtp.password") 142 | 143 | addr := fmt.Sprintf("%s:%d", host, port) 144 | auth := smtp.PlainAuth("", username, password, host) 145 | to := []string{m.Address} 146 | 147 | message := []byte(fmt.Sprintf("From: %s\r\n", username) + 148 | fmt.Sprintf("To: %s\r\n", m.Address) + 149 | fmt.Sprintf("Subject: WinAFL Pet found %d new crashes for job %s\r\n", len(crashes), j.Name) + 150 | "\r\n" + 151 | "WinAFL Pet has found the following crashes since the last check:\r\n\r\n") 152 | 153 | for _, crash := range crashes { 154 | filePath := strings.Split(crash.Args, "\\") 155 | fileName := filePath[len(filePath)-1] 156 | message = append(message, fmt.Sprintf("%s\r\n", fileName)...) 157 | } 158 | 159 | err := smtp.SendMail(addr, auth, username, to, message) 160 | if err != nil { 161 | log.Println(err) 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /server/auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | jwt "github.com/appleboy/gin-jwt/v2" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | const ( 12 | identityKey = "identity" 13 | redirectCode = "" 14 | ) 15 | 16 | type creds struct { 17 | Username string `form:"username" json:"username" binding:"required"` 18 | Password string `form:"password" json:"password" binding:"required"` 19 | } 20 | 21 | func Authentication() (*jwt.GinJWTMiddleware, error) { 22 | middleware, err := jwt.New(&jwt.GinJWTMiddleware{ 23 | Realm: "WinAFL Pet", 24 | Key: generateSecretKey(), 25 | Timeout: time.Hour, 26 | MaxRefresh: time.Hour, 27 | IdentityKey: identityKey, 28 | LoginResponse: func(c *gin.Context, code int, token string, expire time.Time) { 29 | c.Redirect(http.StatusFound, "/jobs/view") 30 | }, 31 | LogoutResponse: func(c *gin.Context, code int) { 32 | c.Data(http.StatusOK, "text/html", []byte(redirectCode)) 33 | }, 34 | PayloadFunc: func(data interface{}) jwt.MapClaims { 35 | if v, ok := data.(*User); ok { 36 | return jwt.MapClaims{ 37 | identityKey: v.UserName, 38 | } 39 | } 40 | return jwt.MapClaims{} 41 | }, 42 | IdentityHandler: func(c *gin.Context) interface{} { 43 | claims := jwt.ExtractClaims(c) 44 | user := newUser() 45 | user.UserName = claims[identityKey].(string) 46 | user.LoadByUsername() 47 | return user 48 | }, 49 | Authenticator: func(c *gin.Context) (interface{}, error) { 50 | var creds creds 51 | if err := c.ShouldBind(&creds); err != nil { 52 | return "", jwt.ErrMissingLoginValues 53 | } 54 | user := newUser() 55 | user.UserName = creds.Username 56 | if err := user.LoadByUsername(); err == nil { 57 | if ok, err := comparePassword(creds.Password, user.Password); ok && err == nil { 58 | return user, nil 59 | } 60 | } 61 | return nil, jwt.ErrFailedAuthentication 62 | }, 63 | Unauthorized: func(c *gin.Context, code int, message string) { 64 | if message == "incorrect Username or Password" { 65 | c.HTML(http.StatusUnauthorized, "user_login", gin.H{ 66 | "alert": "Invalid username or password.", 67 | "context": "danger", 68 | }) 69 | return 70 | } 71 | c.Data(http.StatusUnauthorized, "text/html", []byte(redirectCode)) 72 | }, 73 | SendCookie: true, 74 | SecureCookie: !gin.IsDebugging(), 75 | CookieHTTPOnly: true, 76 | CookieName: "token", 77 | TokenLookup: "header: Authorization, cookie: token", 78 | }) 79 | 80 | return middleware, err 81 | } 82 | -------------------------------------------------------------------------------- /server/auth_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestAuthRoute(t *testing.T) { 16 | router := setupRouter() 17 | 18 | data := url.Values{} 19 | data.Set("username", "wrongusername") 20 | data.Set("password", "wrongpassword") 21 | 22 | w := httptest.NewRecorder() 23 | req, _ := http.NewRequest("POST", "/user/login", strings.NewReader(data.Encode())) 24 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 25 | router.ServeHTTP(w, req) 26 | 27 | resp := w.Result() 28 | body, _ := ioutil.ReadAll(resp.Body) 29 | 30 | assert.Equal(t, 401, resp.StatusCode) 31 | assert.Contains(t, string(body), "Invalid username or password.") 32 | } 33 | -------------------------------------------------------------------------------- /server/crash.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "path/filepath" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | 12 | sq "github.com/Masterminds/squirrel" 13 | "github.com/gin-gonic/gin" 14 | "github.com/parnurzeal/gorequest" 15 | "github.com/rs/xid" 16 | "github.com/sgabe/structable" 17 | ) 18 | 19 | const ( 20 | TB_NAME_CRASHES = "crashes" 21 | TB_SCHEMA_CRASHES = ` 22 | CREATE TABLE crashes ( 23 | "id" INTEGER PRIMARY KEY AUTOINCREMENT, 24 | "jid" INTEGER, 25 | "guid" TEXT UNIQUE, 26 | "label" TEXT NOT NULL, 27 | "fuzzerid" TEXT, 28 | "bugid" TEXT, 29 | "mod" TEXT, 30 | "func" TEXT, 31 | "desc" TEXT, 32 | "imp" TEXT, 33 | "args" TEXT, 34 | "verified" INTEGER, 35 | FOREIGN KEY (jid) REFERENCES jobs(id) 36 | );` 37 | ) 38 | 39 | type Crash struct { 40 | structable.Recorder 41 | ID int `stbl:"id, PRIMARY_KEY, AUTO_INCREMENT"` 42 | JobID int `json:"jid" stbl:"jid"` 43 | GUID xid.ID `json:"guid" stbl:"guid"` 44 | JobGUID xid.ID `json:"jguid"` 45 | Label string `json:"label" form:"label" stbl:"label"` 46 | FuzzerID string `json:"fuzzerid" form:"fuzzerid" stbl:"fuzzerid"` 47 | BugID string `json:"bugid" form:"bugid" stbl:"bugid"` 48 | Module string `json:"mod" form:"mod" stbl:"mod"` 49 | Function string `json:"func" form:"func" stbl:"func"` 50 | Description string `json:"desc" form:"desc" stbl:"desc"` 51 | Impact string `json:"imp" form:"imp" stbl:"imp"` 52 | Args string `json:"args" form:"args" stbl:"args"` 53 | Verified bool `stbl:"verified" form:"verified"` 54 | } 55 | 56 | func newCrash() *Crash { 57 | c := new(Crash) 58 | c.GUID = xid.New() 59 | c.Recorder = structable.New(db, DB_FLAVOR).Bind(TB_NAME_CRASHES, c) 60 | return c 61 | } 62 | 63 | func (c *Crash) LoadByGUID() error { 64 | return c.Recorder.LoadWhere("guid = ?", c.GUID) 65 | } 66 | 67 | func (c *Crash) LoadByJobIDArgs() error { 68 | return c.Recorder.LoadWhere("jid = ? and args = ?", c.JobID, c.Args) 69 | } 70 | 71 | func (c *Crash) GetJob() (*Job, error) { 72 | j := newJob() 73 | j.ID = c.JobID 74 | if err := j.Load(); err != nil { 75 | return j, err 76 | } 77 | return j, nil 78 | } 79 | 80 | func (c *Crash) GetRisk() string { 81 | risk := "none" 82 | 83 | re := regexp.MustCompile(`\w{2,3}(R|W|E)\W?`) 84 | matches := re.FindStringSubmatch(c.BugID) 85 | if matches == nil { 86 | return risk 87 | } 88 | 89 | switch matches[1] { 90 | case "R": 91 | risk = "low" 92 | case "W": 93 | risk = "medium" 94 | case "E": 95 | risk = "high" 96 | } 97 | 98 | return risk 99 | } 100 | 101 | func loadCrashes(page uint64) ([]*Crash, error) { 102 | c := &Crash{} 103 | sc := structable.New(db, DB_FLAVOR).Bind(TB_NAME_CRASHES, c) 104 | 105 | fn := func(d structable.Describer, q sq.SelectBuilder) (sq.SelectBuilder, error) { 106 | return q.OrderBy("id DESC").Limit(99).Offset(page * 99), nil 107 | } 108 | 109 | items, err := listWhere(sc, fn) 110 | if err != nil { 111 | return []*Crash{}, err 112 | } 113 | 114 | // Because we get back a []Recorder, we need to get the original data 115 | // back out. We have to manually convert it back to its real type. 116 | crashes := make([]*Crash, len(items)) 117 | for i, item := range items { 118 | crashes[i] = item.Interface().(*Crash) 119 | } 120 | 121 | return crashes, err 122 | } 123 | 124 | func viewCrashes(c *gin.Context) { 125 | title := "Crashes" 126 | currentPage := 0 127 | 128 | p, err := strconv.Atoi(c.DefaultQuery("p", "1")) 129 | if err == nil && p > 0 && p < (totalPages()+1) { 130 | currentPage = p - 1 131 | } 132 | 133 | crashes, err := loadCrashes(uint64(currentPage)) 134 | if err != nil { 135 | otherError(c, map[string]string{ 136 | "title": title, 137 | "alert": err.Error(), 138 | "template": "crashes_view", 139 | }) 140 | return 141 | } 142 | 143 | c.HTML(http.StatusOK, "crashes_view", gin.H{ 144 | "title": title, 145 | "crashes": crashes, 146 | "currentPage": currentPage, 147 | }) 148 | } 149 | 150 | func deleteCrashes(c *gin.Context) { 151 | b := sq.Delete("").From(TB_NAME_CRASHES).RunWith(db) 152 | 153 | _, err := b.Exec() 154 | if err != nil { 155 | otherError(c, map[string]string{"alert": err.Error()}) 156 | return 157 | } 158 | 159 | c.JSON(http.StatusOK, gin.H{ 160 | "alert": "All crash records have been successfully deleted!", 161 | "context": "success", 162 | }) 163 | } 164 | 165 | func verifyCrash(c *gin.Context) { 166 | crash := newCrash() 167 | crash.GUID, _ = xid.FromString(c.Param("guid")) 168 | if err := crash.LoadByGUID(); err != nil { 169 | otherError(c, map[string]string{"alert": err.Error()}) 170 | return 171 | } 172 | 173 | j := newJob() 174 | j.ID = crash.JobID 175 | j.Load() 176 | 177 | a, _ := j.GetAgent() 178 | crash.JobGUID = j.GUID 179 | 180 | request := gorequest.New() 181 | request.Debug = false 182 | 183 | targetURL := fmt.Sprintf("http://%s:%d/crash/%s/verify", a.Host, a.Port, crash.GUID) 184 | _, bodyBytes, errs := request.Post(targetURL).Set("X-Auth-Key", a.Key).Send(crash).EndStruct(&crash) 185 | if errs != nil { 186 | otherError(c, map[string]string{"alert": errs[0].Error()}) 187 | return 188 | } 189 | 190 | resp := APIResponse{} 191 | if err := json.Unmarshal(bodyBytes, &resp); err != nil { 192 | otherError(c, map[string]string{"alert": err.Error()}) 193 | return 194 | } 195 | 196 | if len(resp.Err) > 0 { 197 | otherError(c, map[string]string{"alert": resp.Err}) 198 | return 199 | } 200 | 201 | crash.Verified = true 202 | 203 | if err := crash.Update(); err != nil { 204 | otherError(c, map[string]string{"alert": err.Error()}) 205 | return 206 | } 207 | 208 | c.JSON(http.StatusOK, gin.H{ 209 | "alert": fmt.Sprintf("A(n) %s bug was detected in %s at %s!", crash.BugID, crash.Module, crash.Function), 210 | "context": "success", 211 | }) 212 | } 213 | 214 | func deleteCrash(c *gin.Context) { 215 | crash := newCrash() 216 | crash.GUID, _ = xid.FromString(c.Param("guid")) 217 | if err := crash.LoadByGUID(); err != nil { 218 | otherError(c, map[string]string{"alert": err.Error()}) 219 | return 220 | } 221 | 222 | if err := crash.Delete(); err != nil { 223 | otherError(c, map[string]string{"alert": err.Error()}) 224 | return 225 | } 226 | 227 | c.JSON(http.StatusOK, gin.H{ 228 | "alert": "Crash successfully deleted!", 229 | "context": "success", 230 | }) 231 | } 232 | 233 | func editCrash(c *gin.Context) { 234 | title := "Edit crash" 235 | 236 | crash := newCrash() 237 | crash.GUID, _ = xid.FromString(c.Param("guid")) 238 | 239 | switch c.Request.Method { 240 | case http.MethodGet: 241 | if err := crash.LoadByGUID(); err != nil { 242 | otherError(c, map[string]string{ 243 | "alert": err.Error(), 244 | "template": "crash_edit", 245 | }) 246 | return 247 | } 248 | c.HTML(http.StatusOK, "crash_edit", gin.H{ 249 | "title": title, 250 | "crash": crash, 251 | }) 252 | case http.MethodPost: 253 | if err := crash.LoadByGUID(); err != nil { 254 | otherError(c, map[string]string{"alert": err.Error()}) 255 | return 256 | } 257 | if err := c.ShouldBind(&crash); err != nil { 258 | otherError(c, map[string]string{ 259 | "title": title, 260 | "alert": err.Error(), 261 | "template": "job_edit", 262 | }) 263 | return 264 | } 265 | if err := crash.Update(); err != nil { 266 | otherError(c, map[string]string{ 267 | "title": title, 268 | "alert": err.Error(), 269 | "template": "crash_edit", 270 | }) 271 | return 272 | } 273 | c.Redirect(http.StatusFound, "/crashes/view") 274 | default: 275 | c.JSON(http.StatusInternalServerError, gin.H{}) 276 | } 277 | } 278 | 279 | func downloadCrash(c *gin.Context) { 280 | crash := newCrash() 281 | crash.GUID, _ = xid.FromString(c.Param("guid")) 282 | if err := crash.LoadByGUID(); err != nil { 283 | otherError(c, map[string]string{"alert": err.Error()}) 284 | return 285 | } 286 | 287 | j := newJob() 288 | j.ID = crash.JobID 289 | j.Load() 290 | 291 | a, _ := j.GetAgent() 292 | crash.JobGUID = j.GUID 293 | 294 | request := gorequest.New() 295 | request.Debug = false 296 | 297 | targetURL := fmt.Sprintf("http://%s:%d/crash/%s/download", a.Host, a.Port, crash.GUID) 298 | resp, bodyBytes, errs := request.Post(targetURL).Set("X-Auth-Key", a.Key).Send(crash).EndBytes() 299 | if errs != nil { 300 | otherError(c, map[string]string{"alert": errs[0].Error()}) 301 | return 302 | } 303 | 304 | if resp.StatusCode != http.StatusOK { 305 | resp := APIResponse{} 306 | if err := json.Unmarshal(bodyBytes, &resp); err != nil { 307 | otherError(c, map[string]string{ 308 | "alert": err.Error(), 309 | "context": "danger", 310 | }) 311 | return 312 | } 313 | 314 | if len(resp.Err) > 0 { 315 | otherError(c, map[string]string{ 316 | "alert": resp.Err, 317 | "context": "danger", 318 | }) 319 | return 320 | } 321 | } 322 | 323 | c.Header("Content-Transfer-Encoding", "binary") 324 | c.Header("Content-Disposition", "attachment; filename="+filepath.Base(strings.Replace(crash.Args, "\\", "/", -1))) 325 | 326 | c.Data(http.StatusOK, "application/octet-stream", bodyBytes) 327 | } 328 | -------------------------------------------------------------------------------- /server/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /server/db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | 10 | "github.com/Masterminds/squirrel" 11 | _ "github.com/mattn/go-sqlite3" 12 | "github.com/sgabe/structable" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | const ( 17 | DB_FLAVOR = "sqlite3" 18 | DB_SOURCE = "database.db" 19 | ) 20 | 21 | func createDB(dataDir string, dataSrc string) { 22 | log.Println("Creating database file") 23 | 24 | if err := os.MkdirAll(dataDir, os.ModePerm); err != nil { 25 | log.Fatal(err.Error()) 26 | } 27 | 28 | file, err := os.Create(dataSrc) 29 | if err != nil { 30 | log.Fatal(err.Error()) 31 | } 32 | file.Close() 33 | log.Println("Database file created") 34 | } 35 | 36 | func initDB(dataType string, dataSrc string) { 37 | con, _ := sql.Open(dataType, dataSrc) 38 | defer con.Close() 39 | 40 | aStatements := map[string]string{ 41 | TB_NAME_AGENTS: TB_SCHEMA_AGENTS, 42 | TB_NAME_JOBS: TB_SCHEMA_JOBS, 43 | TB_NAME_CRASHES: TB_SCHEMA_CRASHES, 44 | TB_NAME_STATS: TB_SCHEMA_STATS, 45 | TB_NAME_USERS: TB_SCHEMA_USERS, 46 | } 47 | 48 | for n, s := range aStatements { 49 | log.Printf("Creating '%s' table\n", n) 50 | statement, err := con.Prepare(s) 51 | if err != nil { 52 | log.Fatal(err.Error()) 53 | } 54 | statement.Exec() 55 | log.Printf("Table '%s' created\n", n) 56 | } 57 | } 58 | 59 | func getDB() squirrel.DBProxyBeginner { 60 | dataType := DB_FLAVOR 61 | dataDir := viper.GetString("data.dir") 62 | dataSrc := filepath.Join(dataDir, DB_SOURCE) 63 | 64 | if !fileExists(dataSrc) { 65 | createDB(dataDir, dataSrc) 66 | initDB(dataType, dataSrc) 67 | initUser() 68 | } 69 | 70 | con, _ := sql.Open(dataType, dataSrc) 71 | cache := squirrel.NewStmtCacheProxy(con) 72 | 73 | return cache 74 | } 75 | 76 | func listWhere(d structable.Recorder, fn structable.WhereFunc) ([]structable.Recorder, error) { 77 | var tn string = d.TableName() 78 | var cols []string = d.Columns(true) 79 | buf := []structable.Recorder{} 80 | 81 | // Base query 82 | q := d.Builder().Select(cols...).From(tn) 83 | 84 | // Allow the fn to modify our query 85 | var err error 86 | q, err = fn(d, q) 87 | if err != nil { 88 | return buf, err 89 | } 90 | 91 | rows, err := q.Query() 92 | if err != nil || rows == nil { 93 | return buf, err 94 | } 95 | defer rows.Close() 96 | 97 | v := reflect.Indirect(reflect.ValueOf(d)) 98 | t := v.Type() 99 | for rows.Next() { 100 | nv := reflect.New(t) 101 | 102 | // Bind an empty base object. Basically, we fetch the object out of 103 | // the DbRecorder, and then construct an empty one. 104 | rec := reflect.New(reflect.Indirect(reflect.ValueOf(d.(*structable.DbRecorder).Interface())).Type()) 105 | nv.Interface().(structable.Recorder).Bind(d.TableName(), rec.Interface()) 106 | 107 | s := nv.Interface().(structable.Recorder) 108 | s.Init(d.DB(), d.Driver()) 109 | dest := s.FieldReferences(true) 110 | 111 | if err := rows.Scan(dest...); err != nil { 112 | return buf, err 113 | } 114 | 115 | buf = append(buf, s) 116 | } 117 | 118 | return buf, rows.Err() 119 | } 120 | -------------------------------------------------------------------------------- /server/db_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/spf13/viper" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCreateDB(t *testing.T) { 12 | dataDir := viper.GetString("data.dir") 13 | dataSrc := filepath.Join(dataDir, DB_SOURCE) 14 | assert.True(t, fileExists(dataSrc)) 15 | } 16 | -------------------------------------------------------------------------------- /server/go.mod: -------------------------------------------------------------------------------- 1 | module server 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/Arafatk/glot v0.0.0-20180312013246-79d5219000f0 7 | github.com/Masterminds/goutils v1.1.1 // indirect 8 | github.com/Masterminds/semver v1.5.0 // indirect 9 | github.com/Masterminds/sprig v2.22.0+incompatible 10 | github.com/Masterminds/squirrel v1.5.1 11 | github.com/appleboy/gin-jwt/v2 v2.7.0 12 | github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e 13 | github.com/elazarl/goproxy v1.2.1 // indirect 14 | github.com/gin-contrib/multitemplate v0.0.0-20211002122701-e9e3201b87a0 15 | github.com/gin-gonic/gin v1.9.1 16 | github.com/google/uuid v1.3.0 // indirect 17 | github.com/huandu/xstrings v1.3.2 // indirect 18 | github.com/imdario/mergo v0.3.12 // indirect 19 | github.com/kr/pretty v0.3.0 // indirect 20 | github.com/mattn/go-sqlite3 v1.14.22 21 | github.com/mitchellh/copystructure v1.2.0 // indirect 22 | github.com/parnurzeal/gorequest v0.2.16 23 | github.com/pkg/errors v0.9.1 // indirect 24 | github.com/rogpeppe/go-internal v1.8.0 // indirect 25 | github.com/rs/xid v1.3.0 26 | github.com/sgabe/structable v0.0.0-20170407152004-a1a302ef78ec 27 | github.com/smartystreets/goconvey v1.6.4 // indirect 28 | github.com/spf13/cast v1.4.1 29 | github.com/spf13/pflag v1.0.5 30 | github.com/spf13/viper v1.9.0 31 | github.com/stretchr/testify v1.8.3 32 | github.com/ugorji/go v1.2.6 // indirect 33 | golang.org/x/crypto v0.35.0 34 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 35 | moul.io/http2curl v1.0.0 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/Masterminds/squirrel" 10 | "github.com/gin-gonic/gin" 11 | flag "github.com/spf13/pflag" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | const ( 16 | DEFAULT_LOG = "winaflpet.log" 17 | 18 | DEFAULT_SERVER_HOST = "127.0.0.1" 19 | DEFAULT_SERVER_PORT = 4141 20 | 21 | DEFAULT_DATA_DIR = "data" 22 | DEFAULT_DATABASE_TYPE = "sqlite3" 23 | DEFAULT_DATABASE_NAME = "database.db" 24 | 25 | DEFAULT_USER_NAME = "admin" 26 | 27 | DEFAULT_ALERT_INTERVAL = 10 28 | ) 29 | 30 | var ( 31 | alert Alert 32 | BuildVer string 33 | BuildRev string 34 | db squirrel.DBProxyBeginner 35 | ) 36 | 37 | func setDefaults() { 38 | viper.SetDefault("data.dir", DEFAULT_DATA_DIR) 39 | viper.BindEnv("data.dir", "WINAFLPET_DATA") 40 | 41 | viper.SetDefault("server.host", DEFAULT_SERVER_HOST) 42 | viper.BindEnv("server.host", "WINAFLPET_HOST") 43 | 44 | viper.SetDefault("server.port", DEFAULT_SERVER_PORT) 45 | viper.BindEnv("server.port", "WINAFLPET_PORT") 46 | 47 | viper.SetDefault("log", DEFAULT_LOG) 48 | viper.BindEnv("log", "WINAFLPET_LOG") 49 | 50 | viper.SetDefault("alert.interval", DEFAULT_ALERT_INTERVAL) 51 | viper.BindEnv("alert.interval", "WINAFLPET_ALERT_INTERVAL") 52 | 53 | viper.BindEnv("smtp.host", "WINAFLPET_SMTP_HOST") 54 | viper.BindEnv("smtp.port", "WINAFLPET_SMTP_PORT") 55 | viper.BindEnv("smtp.username", "WINAFLPET_SMTP_USERNAME") 56 | viper.BindEnv("smtp.password", "WINAFLPET_SMTP_PASSWORD") 57 | } 58 | 59 | func main() { 60 | var ( 61 | host string 62 | port int 63 | log string 64 | config string 65 | version bool 66 | debug bool 67 | ) 68 | 69 | setDefaults() 70 | 71 | flag.StringVarP(&host, "host", "h", DEFAULT_SERVER_HOST, "Host to bind to") 72 | viper.BindPFlag("server.host", flag.Lookup("host")) 73 | 74 | flag.IntVarP(&port, "port", "p", DEFAULT_SERVER_PORT, "Port to bind to") 75 | viper.BindPFlag("server.port", flag.Lookup("port")) 76 | 77 | flag.StringVarP(&log, "log", "l", DEFAULT_LOG, "Log filename") 78 | viper.BindPFlag("log", flag.Lookup("log")) 79 | 80 | flag.StringVarP(&config, "config", "c", "", "Configuration filename") 81 | flag.BoolVarP(&version, "version", "v", false, "Output the current version of the server") 82 | flag.BoolVarP(&debug, "debug", "d", false, "Enable debug mode") 83 | 84 | flag.Parse() 85 | 86 | if version { 87 | fmt.Printf("WinAFL Pet Server v%s (rev %s)\n", BuildVer, BuildRev) 88 | os.Exit(0) 89 | } 90 | 91 | if config != "" { 92 | viper.SetConfigFile(config) 93 | } else { 94 | viper.SetConfigName("winaflpet") 95 | viper.SetConfigType("yaml") 96 | viper.AddConfigPath(viper.GetString("data.dir")) 97 | } 98 | 99 | if err := viper.ReadInConfig(); err != nil { 100 | if config != "" { 101 | fmt.Println(err) 102 | } 103 | } 104 | 105 | db = getDB() 106 | 107 | gin.DisableConsoleColor() 108 | gin.SetMode(gin.ReleaseMode) 109 | 110 | if debug { 111 | gin.SetMode(gin.DebugMode) 112 | } 113 | 114 | f, _ := os.Create(filepath.Join(viper.GetString("data.dir"), viper.GetString("log"))) 115 | gin.DefaultWriter = io.MultiWriter(f, os.Stdout) 116 | 117 | r := setupRouter() 118 | addr := fmt.Sprintf("%s:%d", viper.GetString("server.host"), viper.GetInt("server.port")) 119 | if err := r.Run(addr); err != nil { 120 | fmt.Println(err) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /server/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "testing" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestMain(m *testing.M) { 14 | setDefaults() 15 | db = getDB() 16 | 17 | gin.DisableConsoleColor() 18 | gin.SetMode(gin.ReleaseMode) 19 | 20 | exitVal := m.Run() 21 | 22 | os.Exit(exitVal) 23 | } 24 | 25 | func TestPing(t *testing.T) { 26 | router := setupRouter() 27 | 28 | w := httptest.NewRecorder() 29 | req, _ := http.NewRequest("POST", "/ping", nil) 30 | router.ServeHTTP(w, req) 31 | 32 | assert.Equal(t, 200, w.Code) 33 | assert.Equal(t, "pong", w.Body.String()) 34 | } 35 | -------------------------------------------------------------------------------- /server/plot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/Arafatk/glot" 10 | ) 11 | 12 | const ( 13 | GNUPLOT_CMDS = ` 14 | set terminal png truecolor enhanced size 1000,300 butt 15 | set output 'public/plots/%[1]v/%[2]v/high_freq.png' 16 | 17 | set tics font 'small' 18 | unset mxtics 19 | unset mytics 20 | 21 | set grid xtics linetype 0 linecolor rgb '#e0e0e0' 22 | set grid ytics linetype 0 linecolor rgb '#e0e0e0' 23 | set border linecolor rgb '#50c0f0' 24 | set tics textcolor rgb '#000000' 25 | set key outside 26 | 27 | set autoscale xfixmin 28 | set autoscale xfixmax 29 | 30 | set xlabel "relative time in seconds" font "small" 31 | 32 | plot 'public/plots/%[1]v/%[2]v/plot_data' using 1:4 with filledcurve x1 title 'total paths' linecolor rgb '#000000' fillstyle transparent solid 0.2 noborder, \ 33 | '' using 1:3 with filledcurve x1 title 'current path' linecolor rgb '#f0f0f0' fillstyle transparent solid 0.5 noborder, \ 34 | '' using 1:5 with lines title 'pending paths' linecolor rgb '#0090ff' linewidth 3, \ 35 | '' using 1:6 with lines title 'pending favs' linecolor rgb '#c00080' linewidth 3, \ 36 | '' using 1:2 with lines title 'cycles done' linecolor rgb '#c000f0' linewidth 3 37 | 38 | set terminal png truecolor enhanced size 1000,200 butt 39 | set output 'public/plots/%[1]v/%[2]v/low_freq.png' 40 | 41 | plot 'public/plots/%[1]v/%[2]v/plot_data' using 1:8 with filledcurve x1 title '' linecolor rgb '#c00080' fillstyle transparent solid 0.2 noborder, \ 42 | '' using 1:8 with lines title ' uniq crashes' linecolor rgb '#c00080' linewidth 3, \ 43 | '' using 1:9 with lines title 'uniq hangs' linecolor rgb '#c000f0' linewidth 3, \ 44 | '' using 1:10 with lines title 'levels' linecolor rgb '#0090ff' linewidth 3 45 | 46 | set terminal png truecolor enhanced size 1000,200 butt 47 | set output 'public/plots/%[1]v/%[2]v/exec_speed.png' 48 | 49 | plot 'public/plots/%[1]v/%[2]v/plot_data' using 1:11 with filledcurve x1 title '' linecolor rgb '#0090ff' fillstyle transparent solid 0.2 noborder, \ 50 | 'public/plots/%[1]v/%[2]v/plot_data' using 1:11 with lines title ' execs/sec' linecolor rgb '#0090ff' linewidth 3 smooth bezier;` 51 | ) 52 | 53 | type Plot struct { 54 | RelativeTime int `json:"relative_time"` 55 | CyclesDone int `json:"cycles_done"` 56 | CurPath int `json:"cur_path"` 57 | PathsTotal int `json:"paths_total"` 58 | PendingTotal int `json:"pending_total"` 59 | PendingFavs int `json:"pending_favs"` 60 | MapSize int `json:"map_size"` 61 | UniqueCrashes int `json:"unique_crashes"` 62 | UniqueHangs int `json:"unique_hangs"` 63 | MaxDepth string `json:"max_depth"` 64 | ExecsPerSec float64 `json:"execs_per_sec"` 65 | } 66 | 67 | func createPlots(jGUID string, fuzzerID string) error { 68 | dimensions := 2 69 | persist := false 70 | debug := false 71 | 72 | plot, _ := glot.NewPlot(dimensions, persist, debug) 73 | 74 | plotCmd := fmt.Sprintf(GNUPLOT_CMDS, jGUID, fuzzerID) 75 | if err := plot.Cmd(plotCmd); err != nil { 76 | return err 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func collectPlots(jGUID string, fuzzerID string) ([]string, error) { 83 | var plots []string 84 | 85 | for _, img := range [3]string{"exec_speed.png", "high_freq.png", "low_freq.png"} { 86 | plot := fmt.Sprintf("/plots/%s/%s/%s", jGUID, fuzzerID, img) 87 | if fileEmpty(filepath.Join("public", plot)) { 88 | return plots, errors.New("plot data is not yet available") 89 | } 90 | plots = append(plots, plot) 91 | } 92 | 93 | return plots, nil 94 | } 95 | 96 | func savePlotData(jGUID string, fuzzerID string, data []byte) error { 97 | dirPath := filepath.Join("public", "plots", jGUID, fuzzerID) 98 | if err := os.MkdirAll(dirPath, os.ModePerm); err != nil { 99 | return err 100 | } 101 | 102 | filePath := filepath.Join(dirPath, "plot_data") 103 | if err := os.WriteFile(filePath, data, 0600); err != nil { 104 | return err 105 | } 106 | 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /server/public/plots/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgabe/winaflpet/245b55169a1a870dafc6528caf8dacd568e62a8a/server/public/plots/.gitignore -------------------------------------------------------------------------------- /server/public/static/css/custom.css: -------------------------------------------------------------------------------- 1 | .bd-placeholder-img { 2 | font-size: 1.125rem; 3 | text-anchor: middle; 4 | -webkit-user-select: none; 5 | -moz-user-select: none; 6 | -ms-user-select: none; 7 | user-select: none; 8 | } 9 | 10 | @media (min-width: 768px) { 11 | .bd-placeholder-img-lg { 12 | font-size: 3.5rem; 13 | } 14 | } 15 | 16 | @media (max-width: 768px) { 17 | .card-columns { 18 | -webkit-column-count: 2; 19 | -moz-column-count: 2; 20 | column-count: 2; 21 | } 22 | } 23 | 24 | @media (max-width: 667px) { 25 | main h1 { 26 | margin-top:1.5rem; 27 | } 28 | .form-inline { 29 | width: 100%; 30 | margin-top: 6px; 31 | } 32 | .card { 33 | display: inherit; 34 | } 35 | .card-header h6 .label { 36 | max-width: 180px !important; 37 | } 38 | .card-columns { 39 | -webkit-column-count: 1; 40 | -moz-column-count: 1; 41 | column-count: 1; 42 | } 43 | .card .list-group { 44 | min-height: initial !important; 45 | } 46 | } 47 | 48 | html { 49 | position: relative; 50 | min-height: 100%; 51 | } 52 | 53 | body { 54 | display: -ms-flexbox; 55 | display: flex; 56 | -ms-flex-align: center; 57 | align-items: center; 58 | padding-top: 5rem; 59 | margin-bottom: 100px; 60 | } 61 | 62 | body.login { 63 | padding-top: inherit !important; 64 | background-color: #f5f5f5; 65 | } 66 | 67 | #alert.alert-empty { 68 | display: none; 69 | } 70 | 71 | #wait { 72 | display: none !important; 73 | position: fixed; 74 | height: 32px; 75 | width: 32px; 76 | margin: -16px 0 0 -16px; 77 | top: 50%; 78 | left: 50%; 79 | z-index: 9999; 80 | } 81 | 82 | a.navbar-brand i.fas { 83 | color: orange; 84 | margin-right: 0.25em; 85 | } 86 | 87 | .nav-pills { 88 | float: right; 89 | margin-top: -50px; 90 | } 91 | 92 | .card.job a.action.disabled { 93 | /* opacity: 25%; */ 94 | /* cursor: default; */ 95 | display: none; 96 | } 97 | 98 | .card.crash a.action.float-right { 99 | margin-top: -2px; 100 | margin-left: 0.2em; 101 | } 102 | 103 | .card a.action:hover { 104 | text-decoration: none; 105 | } 106 | 107 | .card { 108 | box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12); 109 | } 110 | 111 | .card .list-group { 112 | min-height: 250px; 113 | } 114 | 115 | .card .list-group-item { 116 | padding-left: 0; 117 | padding-right: 0; 118 | } 119 | 120 | .card-footer { 121 | text-align:right; 122 | padding: .5rem .75rem; 123 | } 124 | 125 | .card-footer .btn { 126 | padding: .1rem .5rem; 127 | border-color:rgba(0,0,0,.125); 128 | } 129 | 130 | .card-footer a.action.float-right { 131 | float:none; 132 | } 133 | 134 | .card-header { 135 | padding: .75rem .75rem; 136 | } 137 | 138 | .card-header .fas { 139 | transition: .3s transform ease-in-out; 140 | } 141 | 142 | .card-header .collapsed .fas { 143 | transform: rotate(90deg); 144 | } 145 | 146 | .card-header .id { 147 | display: block; 148 | float: left; 149 | width: 40px; 150 | padding: 12px 0; 151 | margin: -12px 0 0 -12px; 152 | text-align: center; 153 | } 154 | 155 | .card-header .id.on { background:LightGreen; } 156 | .card-header .id.off { background:LightPink; } 157 | .card-header .id.info { background: LightSkyblue; } 158 | .card-header .id.unknown { background: LightGrey; } 159 | .card-header .id.low { background: LightCyan; } 160 | .card-header .id.medium { background: PeachPuff; } 161 | .card-header .id.high { background: LightPink; } 162 | 163 | .card-header .bugid { 164 | overflow: hidden; 165 | max-width: 159px; 166 | max-height: 1.25rem; 167 | } 168 | 169 | .card-header h6 { 170 | margin: 0 0 0 35px; 171 | } 172 | 173 | .card-header h6 .label { 174 | max-width: 240px; 175 | text-overflow: ellipsis; 176 | overflow: hidden; 177 | white-space: nowrap; 178 | } 179 | 180 | .crash .card-header h6 .label { 181 | float: left; 182 | } 183 | 184 | .card-header h6 small { 185 | margin-left: 5px; 186 | } 187 | 188 | .card-body pre { 189 | display:inline; 190 | font-size: 75%; 191 | } 192 | 193 | .fa-play { 194 | color: green; 195 | } 196 | 197 | .form-signin { 198 | width: 100%; 199 | max-width: 330px; 200 | padding: 15px; 201 | margin: auto; 202 | } 203 | 204 | .form-signin .checkbox { 205 | font-weight: 400; 206 | } 207 | 208 | .form-signin .form-control { 209 | position: relative; 210 | box-sizing: border-box; 211 | height: auto; 212 | padding: 10px; 213 | font-size: 16px; 214 | } 215 | 216 | .form-signin .form-control:focus { 217 | z-index: 2; 218 | } 219 | 220 | .form-signin input[type="email"] { 221 | margin-bottom: -1px; 222 | border-bottom-right-radius: 0; 223 | border-bottom-left-radius: 0; 224 | } 225 | 226 | .form-signin input[type="password"] { 227 | margin-bottom: 10px; 228 | border-top-left-radius: 0; 229 | border-top-right-radius: 0; 230 | } 231 | 232 | footer { 233 | position: absolute; 234 | bottom: 0; 235 | width: 100%; 236 | height: 60px; 237 | line-height: 59px; 238 | background-color: #f5f5f5; 239 | } 240 | 241 | footer .container { 242 | clear:both; 243 | } 244 | 245 | footer .float-right, 246 | footer .float-left { 247 | margin:0; 248 | } 249 | 250 | footer .version:before { 251 | content: "Version "; 252 | font-size: 80%; 253 | } 254 | 255 | @media (max-width: 320px) { 256 | footer .version:before { 257 | content: "v"; 258 | font-size: 80%; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /server/public/static/gif/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgabe/winaflpet/245b55169a1a870dafc6528caf8dacd568e62a8a/server/public/static/gif/demo.gif -------------------------------------------------------------------------------- /server/public/static/js/custom.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | $(document).ajaxStart(function(){ 4 | $("#wait").attr("style", "display: flex !important"); 5 | }); 6 | 7 | $(document).ajaxComplete(function(){ 8 | $("#wait").attr("style", "display: none !important"); 9 | }); 10 | 11 | $(function () { 12 | $('[data-toggle="tooltip"]').tooltip() 13 | }) 14 | 15 | $('.truncate').succinct({ 16 | size: 120 17 | }); 18 | 19 | setInterval(function(){ 20 | $.ajax({ 21 | url: "/user/refresh", 22 | method: "POST" 23 | }) 24 | }, 5*60*1000); 25 | 26 | var getFilename = function(jqXHR) { 27 | var disposition = jqXHR.getResponseHeader("Content-Disposition"); 28 | 29 | if (disposition && disposition.indexOf("attachment") !== -1) { 30 | var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; 31 | var matches = filenameRegex.exec(disposition); 32 | if (matches != null && matches[1]) { 33 | return matches[1].replace(/['"]/g, ""); 34 | } 35 | } 36 | 37 | return ""; 38 | } 39 | 40 | var showAlert = function(context, alert) { 41 | $(".alert").removeClass("alert-empty alert-danger alert-info").addClass("alert-" + context); 42 | $(".alert .message").html(alert); 43 | $("html, body").animate({ scrollTop: 0 }, "fast"); 44 | } 45 | 46 | var toggleActions = function(context) { 47 | $(context).toggleClass("disabled"); 48 | if (!$(context).is(".play")) { 49 | $(context).siblings().toggleClass("disabled"); 50 | } 51 | else if ($(context).is(".last")) { 52 | $(context).siblings(".disabled").not(".play").toggleClass("disabled"); 53 | } 54 | } 55 | 56 | $("a.action").click(function(e) { 57 | var isCustom = $(this).attr("data-method"); 58 | var isDisabled = $(this).is("a.disabled"); 59 | 60 | if (isCustom || isDisabled) { 61 | e.preventDefault(); 62 | } 63 | 64 | if (isCustom && !isDisabled) { 65 | $.ajax({ 66 | url: $(this).attr('href'), 67 | method: $(this).attr("data-method"), 68 | context: $(this), 69 | dataType: $(this).attr("data-type") ? "binary" : "json", 70 | statusCode: { 71 | 401: function() { 72 | setTimeout(function() { 73 | window.location.href = "/user/login"; 74 | }, 1000*2) 75 | } 76 | }, 77 | }).done(function(data, textStatus, jqXHR ) { 78 | if (data.hasOwnProperty("context") && data.hasOwnProperty("alert")) { 79 | showAlert(data.context, data.alert); 80 | if (data.context.includes("success")) { 81 | toggleActions(this); 82 | if ($(this).is("a.plot")) { 83 | var context = $(this); 84 | setTimeout(function() { 85 | window.location.href = context.attr('href'); 86 | }, 1000*5); 87 | } else if ($(this).is("a.verify") || $(this).is("a.play") || $(this).is("a.delete")) { 88 | setTimeout(function() { 89 | location.reload() 90 | }, 1000*5); 91 | } 92 | } 93 | } else { 94 | var blob = new Blob([data], {type: jqXHR.getResponseHeader("Content-Type")}); 95 | var URL = window.URL || window.webkitURL; 96 | var downloadUrl = URL.createObjectURL(blob); 97 | 98 | var filename = getFilename(jqXHR); 99 | if (filename) { 100 | var a = document.createElement("a"); 101 | a.href = downloadUrl; 102 | a.download = filename; 103 | document.body.appendChild(a); 104 | a.click(); 105 | } else { 106 | window.location = downloadUrl; 107 | } 108 | 109 | setTimeout(function () { URL.revokeObjectURL(downloadUrl); }, 100); 110 | } 111 | }).fail(function(jqXHR, textStatus) { 112 | showAlert("danger", "You are not logged in. Please log in and try again."); 113 | }); 114 | } 115 | }); 116 | 117 | $(".close").on("click", function() { 118 | $(".alert").toggleClass("alert-empty"); 119 | }); 120 | 121 | $("#accordion .collapse").on('hide.bs.collapse', function (e) { 122 | $("#"+e.target.id).parent().animate({"padding-top": 0, "padding-bottom": 0}); 123 | }) 124 | 125 | $("#accordion .collapse").on('show.bs.collapse', function (e) { 126 | $("#"+e.target.id).parent().animate({"padding": "1.25rem"}) 127 | }) 128 | 129 | if ($("#plots").length) { 130 | setInterval(function() { 131 | $.post(window.location.href, function(data) {}); 132 | }, 1000*30); 133 | setInterval(function() { 134 | location.reload(); 135 | }, 1000*100); 136 | } 137 | 138 | $("form.create").submit(function(e) { 139 | e.preventDefault(); 140 | 141 | $.ajax({ 142 | url: $(this).attr("action"), 143 | method: "PUT", 144 | data: $(this).serialize(), 145 | context: $(this), 146 | }).done(function(data) { 147 | showAlert(data.context, data.alert); 148 | }); 149 | }); 150 | 151 | $("#autoresume").change(function() { 152 | if($(this).is(":checked")) { 153 | $("#skipCrashes").prop("checked", true); 154 | } 155 | }); 156 | 157 | }); 158 | 159 | $.ajaxTransport("+binary", function(options, originalOptions, jqXHR) { 160 | var isBinary = options.dataType && options.dataType == "binary", 161 | isBlob = options.data && window.Blob && options.data instanceof Blob, 162 | isArrayBuffer = options.data && window.ArrayBuffer && options.data instanceof ArrayBuffer; 163 | if (window.FormData && (isBinary || isArrayBuffer || isBlob)) { 164 | return { 165 | send: function(headers, callback) { 166 | var xhr = new XMLHttpRequest(), 167 | url = options.url, 168 | type = options.type, 169 | async = options.async || true, 170 | dataType = options.responseType || "blob", 171 | data = options.data || null; 172 | 173 | xhr.addEventListener("load", function() { 174 | var data = {}; 175 | data[options.dataType] = xhr.response; 176 | callback(xhr.status, xhr.statusText, data, xhr.getAllResponseHeaders()); 177 | }); 178 | 179 | xhr.open(type, url, async); 180 | xhr.responseType = dataType; 181 | xhr.send(data); 182 | }, 183 | abort: function() { 184 | jqXHR.abort(); 185 | } 186 | }; 187 | } 188 | }); 189 | 190 | jQuery.expr[':'].contains = function(a, i, m) { 191 | return jQuery(a).text().toUpperCase().indexOf(m[3].toUpperCase()) >= 0; 192 | }; 193 | 194 | var filterCards = function() { 195 | $('.card').removeClass('d-none'); 196 | var filter = $("#search").val(); 197 | if (filter) { 198 | $('.card-columns').find('.card .card-body:not(:contains("'+filter+'"))').parent().parent().addClass('d-none'); 199 | } 200 | } 201 | 202 | $(window).on('load', function() { 203 | filterCards(); 204 | }) 205 | 206 | $('#search').on('keyup', function() { 207 | filterCards(); 208 | }) 209 | -------------------------------------------------------------------------------- /server/public/static/js/jquery-succinct-min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Mike King (@micjamking) 3 | * 4 | * jQuery Succinct plugin 5 | * Version 1.1.0 (October 2014) 6 | * 7 | * Licensed under the MIT License 8 | */ 9 | /*global jQuery*/ 10 | !function(a){"use strict";a.fn.succinct=function(b){var c=a.extend({size:240,omission:"...",ignore:!0},b);return this.each(function(){var b,d,e=a(this),f=/[!-\/:-@\[-`{-~]$/,g=function(){e.each(function(){b=a(this).html(),b.length>c.size&&(d=a.trim(b).substring(0,c.size).split(" ").slice(0,-1).join(" "),c.ignore&&(d=d.replace(f,"")),a(this).html(d+c.omission))})};g()})}}(jQuery); -------------------------------------------------------------------------------- /server/public/static/png/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgabe/winaflpet/245b55169a1a870dafc6528caf8dacd568e62a8a/server/public/static/png/schema.png -------------------------------------------------------------------------------- /server/public/static/svg/carrot-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/public/static/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgabe/winaflpet/245b55169a1a870dafc6528caf8dacd568e62a8a/server/public/static/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /server/public/static/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgabe/winaflpet/245b55169a1a870dafc6528caf8dacd568e62a8a/server/public/static/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /server/public/static/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgabe/winaflpet/245b55169a1a870dafc6528caf8dacd568e62a8a/server/public/static/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /server/public/static/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgabe/winaflpet/245b55169a1a870dafc6528caf8dacd568e62a8a/server/public/static/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /server/public/static/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgabe/winaflpet/245b55169a1a870dafc6528caf8dacd568e62a8a/server/public/static/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /server/public/static/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgabe/winaflpet/245b55169a1a870dafc6528caf8dacd568e62a8a/server/public/static/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /server/public/static/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgabe/winaflpet/245b55169a1a870dafc6528caf8dacd568e62a8a/server/public/static/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /server/public/static/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgabe/winaflpet/245b55169a1a870dafc6528caf8dacd568e62a8a/server/public/static/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /server/public/static/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgabe/winaflpet/245b55169a1a870dafc6528caf8dacd568e62a8a/server/public/static/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /server/public/static/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgabe/winaflpet/245b55169a1a870dafc6528caf8dacd568e62a8a/server/public/static/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /server/public/static/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgabe/winaflpet/245b55169a1a870dafc6528caf8dacd568e62a8a/server/public/static/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /server/public/static/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgabe/winaflpet/245b55169a1a870dafc6528caf8dacd568e62a8a/server/public/static/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "net/http" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/Masterminds/sprig" 11 | jwt "github.com/appleboy/gin-jwt/v2" 12 | helmet "github.com/danielkov/gin-helmet" 13 | "github.com/gin-contrib/multitemplate" 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | func customHTMLRender() multitemplate.Renderer { 18 | r := multitemplate.NewRenderer() 19 | 20 | r.AddFromString("50x", "Ooops...") 21 | r.AddFromFiles("user_login", "templates/user_login.html") 22 | 23 | funcs := template.FuncMap{ 24 | "seq": seq, 25 | "hasStatus": hasStatus, 26 | "getVersion": getVersion, 27 | "totalPages": totalPages, 28 | "formatNumber": formatNumber, 29 | "formatDuration": formatDuration, 30 | } 31 | 32 | for k, v := range sprig.FuncMap() { 33 | funcs[k] = v 34 | } 35 | 36 | layouts, err := filepath.Glob("templates/layouts/*.html") 37 | if err != nil { 38 | panic(err.Error()) 39 | } 40 | 41 | includes, err := filepath.Glob("templates/includes/*.html") 42 | if err != nil { 43 | panic(err.Error()) 44 | } 45 | 46 | for _, include := range includes { 47 | fileName := filepath.Base(include) 48 | name := strings.TrimSuffix(fileName, filepath.Ext(fileName)) 49 | layoutCopy := make([]string, len(layouts)) 50 | copy(layoutCopy, layouts) 51 | files := append(layoutCopy, include) 52 | 53 | r.AddFromFilesFuncs(name, funcs, files...) 54 | } 55 | 56 | return r 57 | } 58 | 59 | func home(c *gin.Context) { 60 | c.HTML(http.StatusOK, "home", gin.H{}) 61 | } 62 | 63 | func notFound(c *gin.Context) { 64 | redirectURL := "/user/login" 65 | 66 | claims := jwt.ExtractClaims(c) 67 | if len(claims) != 0 { 68 | redirectURL = "/jobs/view" 69 | } 70 | 71 | redirectCode := fmt.Sprintf("", redirectURL) 72 | 73 | c.Data(http.StatusNotFound, "text/html", []byte(redirectCode)) 74 | } 75 | 76 | func notImplemented(c *gin.Context) { 77 | c.HTML(http.StatusNotImplemented, "50x", gin.H{}) 78 | } 79 | 80 | func otherError(c *gin.Context, p map[string]string) { 81 | alert := "" 82 | if p["alert"] != "" { 83 | alert = p["alert"] 84 | } 85 | 86 | template := "" 87 | if p["template"] != "" { 88 | template = p["template"] 89 | } 90 | 91 | context := "danger" 92 | if p["context"] != "" { 93 | context = p["context"] 94 | } 95 | 96 | title := "" 97 | if p["title"] != "" { 98 | title = p["title"] 99 | } 100 | 101 | if template == "" { 102 | c.JSON(http.StatusOK, gin.H{ 103 | "alert": alert, 104 | "context": context, 105 | }) 106 | return 107 | } 108 | 109 | c.HTML(http.StatusOK, template, gin.H{ 110 | "title": title, 111 | "alert": alert, 112 | "context": context, 113 | }) 114 | } 115 | 116 | func setupRouter() *gin.Engine { 117 | e := gin.Default() 118 | e.Use(helmet.Default()) 119 | e.HTMLRender = customHTMLRender() 120 | 121 | e.Static("/static", "./public/static") 122 | e.Static("/plots", "./public/plots") 123 | e.StaticFile("/favicon.ico", "./public/static/svg/carrot-solid.svg") 124 | 125 | e.POST("/ping", func(c *gin.Context) { 126 | c.Data(http.StatusOK, "text/plain", []byte("pong")) 127 | }) 128 | 129 | auth, err := Authentication() 130 | if err != nil { 131 | fmt.Println("JWT Error:" + err.Error()) 132 | } 133 | 134 | errInit := auth.MiddlewareInit() 135 | if errInit != nil { 136 | fmt.Println("auth.MiddlewareInit() Error:" + errInit.Error()) 137 | } 138 | 139 | e.NoRoute(auth.MiddlewareFunc(), notFound) 140 | e.NoMethod(auth.MiddlewareFunc(), notImplemented) 141 | 142 | u := e.Group("/user") 143 | u.POST("/login", auth.LoginHandler) 144 | u.GET("/login", func(c *gin.Context) { c.HTML(http.StatusOK, "user_login", gin.H{}) }) 145 | 146 | r := e.Group("/") 147 | r.Use(auth.MiddlewareFunc()) 148 | { 149 | r.GET("/", home) 150 | 151 | r.GET("/user/edit", editUser) 152 | r.POST("/user/edit", editUser) 153 | r.GET("/user/logout", auth.LogoutHandler) 154 | r.POST("/user/refresh", auth.RefreshHandler) 155 | 156 | r.GET("/jobs/:action", func(c *gin.Context) { 157 | switch c.Param("action") { 158 | case "create": 159 | createJobs(c) 160 | case "upload": 161 | uploadJobs(c) 162 | case "view": 163 | viewJobs(c) 164 | default: 165 | notFound(c) 166 | } 167 | }) 168 | 169 | r.PUT("/jobs/create", createJobs) 170 | r.POST("/jobs/upload", uploadJobs) 171 | 172 | r.GET("/job/:guid/:action", func(c *gin.Context) { 173 | switch c.Param("action") { 174 | case "view": 175 | viewJob(c) 176 | case "edit": 177 | editJob(c) 178 | case "plot": 179 | plotJob(c) 180 | case "download": 181 | downloadJob(c) 182 | default: 183 | notFound(c) 184 | } 185 | }) 186 | 187 | r.POST("/job/:guid/:action", func(c *gin.Context) { 188 | switch c.Param("action") { 189 | case "start": 190 | startJob(c) 191 | case "stop": 192 | stopJob(c) 193 | case "edit": 194 | editJob(c) 195 | case "check": 196 | checkJob(c) 197 | case "collect": 198 | collectJob(c) 199 | case "plot": 200 | plotJob(c) 201 | case "alert": 202 | alertJob(c) 203 | default: 204 | notImplemented(c) 205 | } 206 | }) 207 | 208 | r.DELETE("/job/:guid", deleteJob) 209 | 210 | r.GET("/crashes/view", viewCrashes) 211 | r.DELETE("/crashes", deleteCrashes) 212 | 213 | r.GET("/crash/:guid/:action", func(c *gin.Context) { 214 | switch c.Param("action") { 215 | case "edit": 216 | editCrash(c) 217 | default: 218 | notFound(c) 219 | } 220 | }) 221 | 222 | r.POST("/crash/:guid/:action", func(c *gin.Context) { 223 | switch c.Param("action") { 224 | case "download": 225 | downloadCrash(c) 226 | case "edit": 227 | editCrash(c) 228 | case "verify": 229 | verifyCrash(c) 230 | default: 231 | notImplemented(c) 232 | } 233 | }) 234 | 235 | r.DELETE("/crash/:guid", deleteCrash) 236 | 237 | r.GET("/agents/:action", func(c *gin.Context) { 238 | switch c.Param("action") { 239 | case "create": 240 | createAgents(c) 241 | case "view": 242 | viewAgents(c) 243 | default: 244 | notFound(c) 245 | } 246 | }) 247 | 248 | r.PUT("/agents/create", createAgents) 249 | r.DELETE("/agents", deleteAgents) 250 | 251 | r.GET("/agent/:guid/:action", func(c *gin.Context) { 252 | switch c.Param("action") { 253 | case "edit": 254 | editAgent(c) 255 | default: 256 | notFound(c) 257 | } 258 | }) 259 | 260 | r.POST("/agent/:guid/:action", func(c *gin.Context) { 261 | switch c.Param("action") { 262 | case "edit": 263 | editAgent(c) 264 | case "check": 265 | checkAgent(c) 266 | default: 267 | notImplemented(c) 268 | } 269 | }) 270 | 271 | r.DELETE("/agent/:guid", deleteAgent) 272 | } 273 | 274 | return e 275 | } 276 | -------------------------------------------------------------------------------- /server/stat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | 7 | "github.com/rs/xid" 8 | "github.com/sgabe/structable" 9 | ) 10 | 11 | const ( 12 | TB_NAME_STATS = "stats" 13 | TB_SCHEMA_STATS = `CREATE TABLE stats ( 14 | "id" INTEGER PRIMARY KEY AUTOINCREMENT, 15 | "jid" INTEGER, 16 | "guid" TEXT UNIQUE, 17 | "fuzzer_pid" INTEGER, 18 | "start_time" INTEGER, 19 | "last_update" INTEGER, 20 | "run_time" INTEGER, 21 | "cycles_done" INTEGER, 22 | "execs_done" INTEGER, 23 | "execs_per_sec" REAL, 24 | "paths_total" INTEGER, 25 | "paths_favored" INTEGER, 26 | "paths_found" INTEGER, 27 | "paths_imported" INTEGER, 28 | "max_depth" INTEGER, 29 | "cur_path" INTEGER, 30 | "pending_favs" INTEGER, 31 | "pending_total" INTEGER, 32 | "variable_paths" INTEGER, 33 | "stability" TEXT, 34 | "bitmap_cvg" TEXT, 35 | "unique_crashes" INTEGER, 36 | "unique_hangs" INTEGER, 37 | "last_path" INTEGER, 38 | "last_crash" INTEGER, 39 | "last_hang" INTEGER, 40 | "execs_since_crash" INTEGER, 41 | "exec_timeout" INTEGER, 42 | "afl_banner" TEXT, 43 | "afl_version" TEXT, 44 | FOREIGN KEY (jid) REFERENCES jobs(id) 45 | );` 46 | ) 47 | 48 | type Stat struct { 49 | structable.Recorder 50 | ID int `stbl:"id, PRIMARY_KEY, AUTO_INCREMENT"` 51 | JobID int `json:"jid" stbl:"jid"` 52 | GUID xid.ID `json:"guid" stbl:"guid"` 53 | FuzzerProcessID int `json:"fuzzer_pid" stbl:"fuzzer_pid"` 54 | StartTime int `json:"start_time" stbl:"start_time"` 55 | LastUpdate int `json:"last_update" stbl:"last_update"` 56 | RunTime int `json:"run_time" stbl:"run_time"` 57 | CyclesDone int `json:"cycles_done" stbl:"cycles_done"` 58 | ExecsDone int `json:"execs_done" stbl:"execs_done"` 59 | ExecsPerSec float64 `json:"execs_per_sec" stbl:"execs_per_sec"` 60 | PathsTotal int `json:"paths_total" stbl:"paths_total"` 61 | PathsFavored int `json:"paths_favored" stbl:"paths_favored"` 62 | PathsFound int `json:"paths_found" stbl:"paths_found"` 63 | PathsImported int `json:"paths_imported" stbl:"paths_imported"` 64 | MaxDepth int `json:"max_depth" stbl:"max_depth"` 65 | CurPath int `json:"cur_path" stbl:"cur_path"` 66 | PendingFavs int `json:"pending_favs" stbl:"pending_favs"` 67 | PendingTotal int `json:"pending_total" stbl:"pending_total"` 68 | VariablePaths int `json:"variable_paths" stbl:"variable_paths"` 69 | Stability string `json:"stability" stbl:"stability"` 70 | BitmapCvg string `json:"bitmap_cvg" stbl:"bitmap_cvg"` 71 | UniqueCrashes int `json:"unique_crashes" stbl:"unique_crashes"` 72 | UniqueHangs int `json:"unique_hangs" stbl:"unique_hangs"` 73 | LastPath int `json:"last_path" stbl:"last_path"` 74 | LastCrash int `json:"last_crash" stbl:"last_crash"` 75 | LastHang int `json:"last_hang" stbl:"last_hang"` 76 | ExecsSinceCrash int `json:"execs_since_crash" stbl:"execs_since_crash"` 77 | ExecTimeout int `json:"exec_timeout" stbl:"exec_timeout"` 78 | AFLBanner string `json:"afl_banner" stbl:"afl_banner"` 79 | AFLVersion string `json:"afl_version" stbl:"afl_version"` 80 | } 81 | 82 | func newStat() *Stat { 83 | s := new(Stat) 84 | s.GUID = xid.New() 85 | s.Recorder = structable.New(db, DB_FLAVOR).Bind(TB_NAME_STATS, s) 86 | return s 87 | } 88 | 89 | // TODO: Find a better way to do this. 90 | func (newStat *Stat) CopyStat(oldStat Stat) { 91 | newStat.FuzzerProcessID = oldStat.FuzzerProcessID 92 | newStat.StartTime = oldStat.StartTime 93 | newStat.RunTime = oldStat.RunTime 94 | newStat.LastUpdate = oldStat.LastUpdate 95 | newStat.CyclesDone = oldStat.CyclesDone 96 | newStat.ExecsDone = oldStat.ExecsDone 97 | newStat.ExecsPerSec = oldStat.ExecsPerSec 98 | newStat.PathsTotal = oldStat.PathsTotal 99 | newStat.PathsFavored = oldStat.PathsFavored 100 | newStat.PathsFound = oldStat.PathsFound 101 | newStat.PathsImported = oldStat.PathsImported 102 | newStat.MaxDepth = oldStat.MaxDepth 103 | newStat.CurPath = oldStat.CurPath 104 | newStat.PendingFavs = oldStat.PendingFavs 105 | newStat.PendingTotal = oldStat.PendingTotal 106 | newStat.VariablePaths = oldStat.VariablePaths 107 | newStat.Stability = oldStat.Stability 108 | newStat.BitmapCvg = oldStat.BitmapCvg 109 | newStat.UniqueCrashes = oldStat.UniqueCrashes 110 | newStat.UniqueHangs = oldStat.UniqueHangs 111 | newStat.LastPath = oldStat.LastPath 112 | newStat.LastCrash = oldStat.LastCrash 113 | newStat.LastHang = oldStat.LastHang 114 | newStat.ExecsSinceCrash = oldStat.ExecsSinceCrash 115 | newStat.ExecTimeout = oldStat.ExecTimeout 116 | newStat.AFLBanner = oldStat.AFLBanner 117 | newStat.AFLVersion = oldStat.AFLVersion 118 | } 119 | 120 | func (s *Stat) GetJob() (*Job, error) { 121 | j := newJob() 122 | j.ID = s.JobID 123 | if err := j.Load(); err != nil { 124 | return j, err 125 | } 126 | return j, nil 127 | } 128 | 129 | func (s *Stat) LoadJobIDFuzzerID() error { 130 | return s.Recorder.LoadWhere("jid = ? and afl_banner = ?", s.JobID, s.AFLBanner) 131 | } 132 | 133 | func (s *Stat) LoadJobIDProcessID() error { 134 | return s.Recorder.LoadWhere("jid = ? and fuzzer_pid = ?", s.JobID, s.FuzzerProcessID) 135 | } 136 | 137 | func (s *Stat) GetFID() int { 138 | fID := 0 139 | 140 | re := regexp.MustCompile(`\d+$`) 141 | matches := re.FindStringSubmatch(s.AFLBanner) 142 | if matches != nil { 143 | fID, _ = strconv.Atoi(matches[0]) 144 | } 145 | 146 | return fID 147 | } 148 | -------------------------------------------------------------------------------- /server/status.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | F1 status = 1 << iota 5 | F2 6 | F3 7 | F4 8 | F5 9 | F6 10 | F7 11 | F8 12 | F9 13 | F10 14 | F11 15 | F12 16 | F13 17 | F14 18 | F15 19 | F16 20 | F17 21 | F18 22 | F19 23 | F20 24 | F21 25 | F22 26 | F23 27 | F24 28 | F25 29 | F26 30 | F27 31 | F28 32 | F29 33 | F30 34 | F31 35 | F32 36 | F33 37 | F34 38 | F35 39 | F36 40 | F37 41 | F38 42 | F39 43 | F40 44 | ) 45 | 46 | type status uint64 47 | 48 | func setStatus(b status, flag status) status { return b | flag } 49 | func clearStatus(b status, flag status) status { return b &^ flag } 50 | func toggleStatus(b status, flag status) status { return b ^ flag } 51 | func hasStatus(b status, i int) bool { return b&statusMap[i] != 0 } 52 | 53 | var statusMap = map[int]status{ 54 | 1: F1, 55 | 2: F2, 56 | 3: F3, 57 | 4: F4, 58 | 5: F5, 59 | 6: F6, 60 | 7: F7, 61 | 8: F8, 62 | 9: F9, 63 | 10: F10, 64 | 11: F11, 65 | 12: F12, 66 | 13: F13, 67 | 14: F14, 68 | 15: F15, 69 | 16: F16, 70 | 17: F17, 71 | 18: F18, 72 | 19: F19, 73 | 20: F20, 74 | 21: F21, 75 | 22: F22, 76 | 23: F23, 77 | 24: F24, 78 | 25: F25, 79 | 26: F26, 80 | 27: F27, 81 | 28: F28, 82 | 29: F29, 83 | 30: F30, 84 | 31: F31, 85 | 32: F32, 86 | 33: F33, 87 | 34: F34, 88 | 35: F35, 89 | 36: F36, 90 | 37: F37, 91 | 38: F38, 92 | 39: F39, 93 | 40: F40, 94 | } 95 | -------------------------------------------------------------------------------- /server/templates/includes/404.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 | Ooops... 3 | {{end}} 4 | -------------------------------------------------------------------------------- /server/templates/includes/agent_edit.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |
3 | 4 |
5 | General settings 6 |
7 | 8 |
9 | 10 |
11 | 12 | Short name of the new job 13 |
14 |
15 | 16 |
17 | 18 |
19 | 20 | Short description of the job 21 |
22 |
23 | 24 |
25 | 26 |
27 | 28 | Active 29 |
30 |
31 | 32 |
33 |
34 | 35 |
36 | Agent settings 37 |
38 | 39 |
40 | 41 |
42 | 43 | Host name or IP address of the remote machine 44 |
45 |
46 | 47 |
48 | 49 |
50 | 51 | Port number of the agent on the remote machine 52 |
53 |
54 | 55 |
56 | 57 |
58 | 59 | Authentication key for API requests 60 |
61 |
62 | 63 |
64 |
65 | 66 | 67 |
68 | {{end}} 69 | -------------------------------------------------------------------------------- /server/templates/includes/agents_create.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |
3 | 4 |
5 | General settings 6 |
7 | 8 |
9 | 10 |
11 | 12 | Short name of the new job 13 |
14 |
15 | 16 |
17 | 18 |
19 | 20 | Short description of the job 21 |
22 |
23 | 24 |
25 | 26 |
27 | 28 | Activate 29 |
30 |
31 | 32 |
33 |
34 | 35 |
36 | Agent settings 37 |
38 | 39 |
40 | 41 |
42 | 43 | Host name or IP address of the remote machine 44 |
45 |
46 | 47 |
48 | 49 |
50 | 51 | Port number of the agent on the remote machine 52 |
53 |
54 | 55 |
56 | 57 |
58 | 59 | Authentication key for API requests 60 |
61 |
62 | 63 |
64 |
65 | 66 | 67 |
68 | {{end}} 69 | -------------------------------------------------------------------------------- /server/templates/includes/agents_view.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 | {{if not .agents}} 3 |

There are no existing agents in the database. Click here to create one!

4 | {{else}} 5 |
6 | {{range .agents}} 7 |
8 | 9 |
10 | {{ .ID }} 11 |
12 | 16 |
17 |
18 | 19 | 29 | 30 | 47 | 48 |
49 | {{end}} 50 |
51 | {{end}} 52 | {{end}} 53 | -------------------------------------------------------------------------------- /server/templates/includes/crash_edit.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |
3 | 4 |
5 |
6 | 7 |
8 | 9 | 10 | Fuzzer ID 11 |
12 | 13 |
14 | 15 | 16 | Bug ID 17 |
18 | 19 |
20 | 21 | 22 | Module 23 |
24 | 25 |
26 | 27 | 28 | Function 29 |
30 | 31 |
32 | 33 | 34 | Label 35 |
36 | 37 |
38 | 39 |
40 | 41 | 42 | Description 43 |
44 | 45 |
46 | 47 | 48 | Args 49 |
50 | 51 |
52 | 53 | 54 |
55 | {{end}} 56 | -------------------------------------------------------------------------------- /server/templates/includes/crashes_view.html: -------------------------------------------------------------------------------- 1 | {{define "pagination"}} 2 | {{if gt totalPages 1}} 3 | {{$currentPage := add . 1 }} 4 | {{$previousPage := sub $currentPage 1}} 5 | {{$nextPage := add $currentPage 1}} 6 | {{$firstPage := sub $currentPage 2 }} 7 | {{$lastPage := add $currentPage 2 }} 8 | 33 | {{end}} 34 | {{end}} 35 | 36 | {{define "verifyCrash"}} 37 | 38 | 39 | 40 | 41 | 42 | {{end}} 43 | 44 | {{define "deleteCrash"}} 45 | 46 | 47 | 48 | 49 | 50 | {{end}} 51 | 52 | {{define "downloadCrash"}} 53 | 54 | 55 | 56 | 57 | 58 | {{end}} 59 | 60 | {{define "editCrash"}} 61 | 62 | 63 | 64 | 65 | 66 | {{end}} 67 | 68 | {{define "content"}} 69 | {{if not .crashes}} 70 |

There are no existing crashes in the database.

71 | {{else}} 72 | 73 | 82 | 83 | {{template "pagination" .currentPage}} 84 |
85 | {{range .crashes}} 86 |
87 |
88 | {{ .ID }} 89 |
90 | {{if .Verified }} 91 | 102 | {{else}} 103 | Unverified 104 | {{ .GetJob.Name }} 105 | {{template "deleteCrash" .GUID}} 106 | {{template "downloadCrash" .GUID}} 107 | {{template "verifyCrash" .GUID}} 108 | {{end}} 109 |
110 |
111 | {{if .Verified }} 112 | 129 | {{end}} 130 |
131 | {{end}} 132 |
133 | {{template "pagination" .currentPage}} 134 | {{end}} 135 | {{end}} 136 | -------------------------------------------------------------------------------- /server/templates/includes/home.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |

Home

3 |

Welcome to WinAFL Pet!

4 | {{end}} 5 | -------------------------------------------------------------------------------- /server/templates/includes/job_edit.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |
3 | 4 |
5 | General settings 6 | 7 |
8 |
9 | 10 |
11 | 17 | Specify the remote agent 18 |
19 |
20 | 21 |
22 | 23 |
24 | 29 | Enable distributed mode by increasing CPU cores 30 |
31 |
32 | 33 |
34 | 35 |
36 | 37 | Short name of the new job 38 |
39 |
40 | 41 |
42 | 43 |
44 | 45 | Short text banner to show on the screen 46 |
47 |
48 |
49 | 50 |
51 |
52 | 53 |
54 | 55 | Short description of the job 56 |
57 |
58 |
59 | 60 |
61 | 62 |
63 | Tool locations 64 |
65 | 66 |
67 | 68 |
69 | 70 | AFL directory on the remote machine 71 |
72 |
73 | 74 |
75 | 76 |
77 | 78 | DynamorIO directory on the remote machine 79 |
80 |
81 | 82 |
83 | 84 |
85 | 86 | Python directory on the remote machine 87 |
88 |
89 | 90 |
91 | 92 |
93 | 94 | BugId directory on the remote machine 95 |
96 |
97 | 98 |
99 |
100 | 101 |
102 | Basic parameters 103 |
104 | 105 |
106 | 107 |
108 | 109 | Input directory with test cases 110 |
111 |
112 | 113 |
114 | 115 |
116 | 117 | Output directory for findings 118 |
119 |
120 | 121 |
122 | 123 |
124 | 128 | Sample delivery mode 129 |
130 |
131 | 132 |
133 | 134 |
135 | 136 | Timeout for each run 137 |
138 |
139 | 140 |
141 | 142 |
143 | 144 |
145 | 146 |
147 | 148 | Library identifying a unique process to attach to 149 |
150 |
151 | 152 |
153 | 154 |
155 | 156 | Path to user-defined library for custom test cases processing 157 |
158 |
159 | 160 |
161 | 162 |
163 | 164 | Optional fuzzer dictionary 165 |
166 |
167 | 168 |
169 | 170 |
171 | 172 | Memory limit for the target process 173 |
174 |
175 | 176 |
177 | 178 |
179 | 180 |
181 |
182 |
183 | 184 | 185 |
186 |
187 |
188 | 189 |
190 |
191 |
192 | 193 | 194 |
195 |
196 |
197 | 198 |
199 |
200 |
201 | 202 | 203 |
204 |
205 |
206 | 207 |
208 |
209 |
210 | 211 | 212 |
213 |
214 |
215 | 216 |
217 | 218 |
219 | 220 |
221 |
222 |
223 | 224 | 225 |
226 |
227 |
228 | 229 |
230 |
231 |
232 | 233 | 234 |
235 |
236 |
237 | 238 |
239 |
240 |
241 | 242 | 243 |
244 |
245 |
246 | 247 |
248 |
249 |
250 | 251 | 252 |
253 |
254 |
255 | 256 |
257 |
258 | 259 |
260 | Environment variables 261 | 262 |
263 | 264 |
265 |
266 |
267 | 268 | 269 |
270 |
271 |
272 | 273 |
274 |
275 |
276 | 277 | 278 |
279 |
280 |
281 | 282 |
283 |
284 |
285 | 286 | 287 |
288 |
289 |
290 | 291 |
292 |
293 |
294 | 295 | 296 |
297 |
298 |
299 | 300 |
301 | 302 |
303 | 304 |
305 | Instrumentation settings 306 | 307 |
308 | 309 |
310 | 311 |
312 | 313 | Module(s) to collect coverage for, separate multiple modules with comma (,) 314 |
315 |
316 | 317 |
318 | 319 |
320 | 324 | The type of coverage being recorded 325 |
326 |
327 | 328 |
329 | 330 |
331 | 332 | Fuzzing iterations to perform 333 |
334 |
335 |
336 | 337 |
338 |
339 | 340 |
341 | 342 | Module with target function to fuzz 343 |
344 |
345 | 346 |
347 | 348 |
349 | 350 | Name of the method to fuzz 351 |
352 |
353 | 354 |
355 | 356 |
357 | 358 | Offset to target function 359 |
360 |
361 | 362 |
363 | 364 |
365 | 366 | Number of arguments the fuzzed method takes 367 |
368 |
369 | 370 |
371 |
372 | 373 |
374 | Target application 375 | 376 |
377 | 378 |
379 | 380 |
381 | 382 | Target application or harness to execute for fuzzing (e.g. harness.exe) 383 |
384 |
385 | 386 |
387 | 388 |
389 | 393 | Target architecture 394 |
395 |
396 | 397 |
398 |
399 | 400 | 401 | 402 | 403 |
404 | {{end}} 405 | -------------------------------------------------------------------------------- /server/templates/includes/job_plot.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |
3 | {{range .plots}} 4 | 5 | {{end}} 6 |
7 | {{end}} 8 | -------------------------------------------------------------------------------- /server/templates/includes/job_view.html: -------------------------------------------------------------------------------- 1 | {{ define "content" }} 2 |
3 |
4 | {{ range .stats }} 5 |
6 | 7 | 13 | 14 |
15 |
16 | 17 |
18 |
Process timing
19 |
20 |
    21 |
  • 22 | Run time: 23 |
    {{ formatDuration .StartTime }}
  • 24 |
  • 25 | Last new path: 26 |
    {{ formatDuration .LastPath }}
  • 27 |
  • 28 | Last unique crash: 29 |
    {{ formatDuration .LastCrash }}
  • 30 |
  • 31 | Last unique hang: 32 |
    {{ formatDuration .LastHang }}
  • 33 |
34 |
35 |
36 | 37 |
38 |
Stage progress
39 |
40 |
    41 |
  • 42 | Total execs: 43 | {{ formatNumber .ExecsDone }}
  • 44 |
  • 45 | Execs since crash: 46 | {{ formatNumber .ExecsSinceCrash }}
  • 47 |
  • 48 | Exec speed: 49 | {{ .ExecsPerSec }}/sec
  • 50 |
51 |
52 |
53 | 54 |
55 |
Overall results
56 |
57 |
    58 |
  • 59 | Cycles done: 60 | {{ .CyclesDone }}
  • 61 |
  • 62 | Total paths: 63 | {{ .PathsTotal }}
  • 64 |
  • 65 | Unique crashes: 66 | {{ .UniqueCrashes }}
  • 67 |
  • 68 | Unique hangs: 69 | {{ .UniqueHangs }}
  • 70 |
  • 71 | Bitmap coverage: 72 | {{ .BitmapCvg }}
  • 73 |
74 |
75 |
76 | 77 |
78 |
Path geometry
79 |
80 |
    81 |
  • 82 | Levels: 83 | {{ .MaxDepth }} 84 |
  • 85 |
  • 86 | Pending: 87 | {{ .PendingTotal }} 88 |
  • 89 |
  • 90 | Pend fav: 91 | {{ .PendingFavs }} 92 |
  • 93 |
  • 94 | Own finds: 95 | {{ .PathsFound }} 96 |
  • 97 |
  • 98 | Imported: 99 | {{ .PathsImported }} 100 |
  • 101 |
  • 102 | Stability: 103 | {{ .Stability }} 104 |
  • 105 |
106 |
107 |
108 | 109 |
110 |
111 | 123 |
124 | 125 | {{end}} 126 |
127 |
128 | {{end}} 129 | -------------------------------------------------------------------------------- /server/templates/includes/jobs_create.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |
3 | 4 |
5 | General settings 6 | 7 |
8 |
9 | 10 |
11 | 20 | Specify the remote agent 21 |
22 |
23 | 24 |
25 | 26 |
27 | 32 | Enable distributed mode by increasing CPU cores 33 |
34 |
35 | 36 |
37 | 38 |
39 | 40 | Short name of the new job 41 |
42 |
43 | 44 |
45 | 46 |
47 | 48 | Short text banner to show on the screen 49 |
50 |
51 |
52 | 53 |
54 |
55 | 56 |
57 | 58 | Short description of the job 59 |
60 |
61 |
62 | 63 |
64 | 65 |
66 | Tool locations 67 |
68 | 69 |
70 | 71 |
72 | 73 | AFL directory on the remote machine 74 |
75 |
76 | 77 |
78 | 79 |
80 | 81 | DynamorIO directory on the remote machine 82 |
83 |
84 | 85 |
86 | 87 |
88 | 89 | Python directory on the remote machine 90 |
91 |
92 | 93 |
94 | 95 |
96 | 97 | BugId directory on the remote machine 98 |
99 |
100 | 101 |
102 |
103 | 104 |
105 | Basic parameters 106 |
107 | 108 |
109 | 110 |
111 | 112 | Input directory with test cases 113 |
114 |
115 | 116 |
117 | 118 |
119 | 120 | Output directory for findings 121 |
122 |
123 | 124 |
125 | 126 |
127 | 131 | Sample delivery mode 132 |
133 |
134 | 135 |
136 | 137 |
138 | 139 | Timeout for each run 140 |
141 |
142 | 143 |
144 | 145 |
146 | 147 |
148 | 149 |
150 | 151 | Library identifying a unique process to attach to 152 |
153 |
154 | 155 |
156 | 157 |
158 | 159 | Path to user-defined library for custom test cases processing 160 |
161 |
162 | 163 |
164 | 165 |
166 | 167 | Optional fuzzer dictionary 168 |
169 |
170 | 171 |
172 | 173 |
174 | 175 | Memory limit for the target process 176 |
177 |
178 | 179 |
180 | 181 |
182 | 183 |
184 |
185 |
186 | 187 | 188 |
189 |
190 |
191 | 192 |
193 |
194 |
195 | 196 | 197 |
198 |
199 |
200 | 201 |
202 |
203 |
204 | 205 | 206 |
207 |
208 |
209 | 210 |
211 |
212 |
213 | 214 | 215 |
216 |
217 |
218 | 219 |
220 | 221 |
222 | 223 |
224 |
225 |
226 | 227 | 228 |
229 |
230 |
231 | 232 |
233 |
234 |
235 | 236 | 237 |
238 |
239 |
240 | 241 |
242 |
243 |
244 | 245 | 246 |
247 |
248 |
249 | 250 |
251 |
252 |
253 | 254 | 255 |
256 |
257 |
258 | 259 |
260 |
261 | 262 |
263 | Environment variables 264 | 265 |
266 | 267 |
268 |
269 |
270 | 271 | 272 |
273 |
274 |
275 | 276 |
277 |
278 |
279 | 280 | 281 |
282 |
283 |
284 | 285 |
286 |
287 |
288 | 289 | 290 |
291 |
292 |
293 | 294 |
295 |
296 |
297 | 298 | 299 |
300 |
301 |
302 | 303 |
304 | 305 |
306 | 307 |
308 | Instrumentation settings 309 | 310 |
311 | 312 |
313 | 314 |
315 | 316 | Module(s) to collect coverage for, separate multiple modules with comma (,) 317 |
318 |
319 | 320 |
321 | 322 |
323 | 327 | The type of coverage being recorded 328 |
329 |
330 | 331 |
332 | 333 |
334 | 335 | Fuzzing iterations to perform 336 |
337 |
338 |
339 | 340 |
341 |
342 | 343 |
344 | 345 | Module with target function to fuzz 346 |
347 |
348 | 349 |
350 | 351 |
352 | 353 | Name of the method to fuzz 354 |
355 |
356 | 357 |
358 | 359 |
360 | 361 | Offset to target function 362 |
363 |
364 | 365 |
366 | 367 |
368 | 369 | Number of arguments the fuzzed method takes 370 |
371 |
372 | 373 |
374 |
375 | 376 |
377 | Target application 378 | 379 |
380 |
381 | 382 |
383 | 384 | Target application or harness to execute for fuzzing (e.g. harness.exe) 385 |
386 |
387 | 388 |
389 | 390 |
391 | 395 | Target architecture 396 |
397 |
398 |
399 |
400 | 401 | 402 | 403 | 404 |
405 | {{end}} 406 | -------------------------------------------------------------------------------- /server/templates/includes/jobs_upload.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |
3 | 4 | 5 |
6 | {{end}} 7 | -------------------------------------------------------------------------------- /server/templates/includes/jobs_view.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 | {{if not .jobs}} 3 |

There are no existing jobs in the database. Click here to create one!

4 | {{else}} 5 |
6 | {{range .jobs}} 7 |
8 |
9 | {{ .ID }} 10 |
11 | 15 |
16 |
17 | 18 | 29 | 30 | 89 |
90 | {{end}} 91 |
92 | {{end}} 93 | {{end}} 94 | -------------------------------------------------------------------------------- /server/templates/includes/user_edit.html: -------------------------------------------------------------------------------- 1 | {{define "content"}} 2 |
3 | 4 |
5 | General information 6 |
7 | 8 |
9 | 10 |
11 | 12 |
13 |
14 | 15 |
16 | 17 |
18 | 19 |
20 |
21 | 22 |
23 | 24 |
25 | 26 |
27 | 28 |
29 | 30 |
31 |
32 | 33 |
34 | 35 |
36 | 37 |
38 |
39 | 40 |
41 | 42 | 43 |
44 | 45 |
46 | Password 47 | 48 |
49 |
50 | 51 |
52 | 53 |
54 |
55 |
56 | 57 |
58 |
59 | 60 |
61 | 62 |
63 |
64 |
65 | 66 |
67 |
68 | 69 |
70 | 71 |
72 |
73 |
74 | 75 |
76 | 77 | 78 |
79 | {{end}} 80 | -------------------------------------------------------------------------------- /server/templates/layouts/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{if .title}}{{.title}}{{else}}Home{{end}} | WinAFL Pet 10 | 11 | 12 | 61 |
62 | {{if .title}}

{{.title}}

{{end}} 63 | 69 |
70 |
71 | Loading... 72 |
73 |
74 | {{template "content" .}} 75 |
76 |
77 |
78 | 79 |

{{ getVersion }}

80 |
81 |
82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /server/templates/user_login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Login | WinAFL Pet 10 | 11 | 12 |
13 | 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /server/user.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | 8 | sq "github.com/Masterminds/squirrel" 9 | jwt "github.com/appleboy/gin-jwt/v2" 10 | "github.com/gin-gonic/gin" 11 | "github.com/sgabe/structable" 12 | ) 13 | 14 | const ( 15 | TB_NAME_USERS = "users" 16 | TB_SCHEMA_USERS = `CREATE TABLE users ( 17 | "id" INTEGER PRIMARY KEY AUTOINCREMENT, 18 | "username" TEXT UNIQUE NOT NULL, 19 | "password" TEXT NOT NULL, 20 | "firstname" TEXT NOT NULL, 21 | "lastname" TEXT NOT NULL, 22 | "email" TEXT NOT NULL 23 | );` 24 | ) 25 | 26 | type User struct { 27 | structable.Recorder 28 | ID int `stbl:"id, PRIMARY_KEY, AUTO_INCREMENT"` 29 | UserName string `json:"username" form:"username" stbl:"username, UNIQUE"` 30 | Password string `json:"password" form:"password" stbl:"password, NOT NULL"` 31 | NewPassword string `json:"newPassword" form:"newPassword"` 32 | NewPasswordConfirmation string `json:"newPasswordConfirmation" form:"newPasswordConfirmation"` 33 | FirstName string `json:"firstname" form:"firstname" stbl:"firstname"` 34 | LastName string `json:"lastname" form:"lastname" stbl:"lastname"` 35 | Email string `json:"email" form:"email" stbl:"email"` 36 | } 37 | 38 | func newUser() *User { 39 | u := new(User) 40 | u.Recorder = structable.New(db, DB_FLAVOR).Bind(TB_NAME_USERS, u) 41 | return u 42 | } 43 | 44 | func (u *User) LoadByUsername() error { 45 | return u.Recorder.LoadWhere("username = ?", u.UserName) 46 | } 47 | 48 | func initUser() { 49 | log.Printf("Creating '%s' user\n", DEFAULT_USER_NAME) 50 | 51 | hostname, err := os.Hostname() 52 | if err != nil { 53 | log.Fatal(err.Error()) 54 | } 55 | 56 | password, err := generatePassword(hostname) 57 | if err != nil { 58 | log.Fatal(err.Error()) 59 | } 60 | 61 | db := getDB() 62 | if _, err := sq.Insert("users"). 63 | Columns("username", "password", "firstname", "lastname", "email"). 64 | Values(DEFAULT_USER_NAME, password, "", "", ""). 65 | RunWith(db).Exec(); err != nil { 66 | log.Fatal(err.Error()) 67 | } 68 | 69 | log.Printf("User '%s' created\n", DEFAULT_USER_NAME) 70 | } 71 | 72 | func editUser(c *gin.Context) { 73 | title := "Edit user" 74 | 75 | claims := jwt.ExtractClaims(c) 76 | user := newUser() 77 | user.UserName = claims[identityKey].(string) 78 | user.LoadByUsername() 79 | 80 | switch c.Request.Method { 81 | case http.MethodGet: 82 | c.HTML(http.StatusOK, "user_edit", gin.H{ 83 | "title": title, 84 | "user": user, 85 | }) 86 | return 87 | case http.MethodPost: 88 | if ok, err := comparePassword(c.PostForm("password"), user.Password); !ok || err != nil { 89 | c.HTML(http.StatusOK, "user_edit", gin.H{ 90 | "title": title, 91 | "alert": "Password invalid!", 92 | "user": user, 93 | "context": "danger", 94 | }) 95 | return 96 | } 97 | 98 | oriPassword := user.Password 99 | if err := c.ShouldBind(&user); err != nil { 100 | otherError(c, map[string]string{ 101 | "title": title, 102 | "alert": err.Error(), 103 | "template": "user_edit", 104 | }) 105 | return 106 | } 107 | user.Password = oriPassword 108 | 109 | if user.NewPassword != "" { 110 | if user.NewPassword != user.NewPasswordConfirmation { 111 | c.HTML(http.StatusOK, "user_edit", gin.H{ 112 | "title": title, 113 | "alert": "The password confirmation does not match.", 114 | "user": user, 115 | "context": "danger", 116 | }) 117 | return 118 | } 119 | user.Password, _ = generatePassword(user.NewPassword) 120 | } 121 | 122 | if err := user.Update(); err != nil { 123 | c.HTML(http.StatusOK, "user_edit", gin.H{ 124 | "title": title, 125 | "alert": err.Error(), 126 | "user": user, 127 | "context": "danger", 128 | }) 129 | return 130 | } 131 | 132 | c.HTML(http.StatusOK, "user_edit", gin.H{ 133 | "title": title, 134 | "alert": "User profile successfully updated.", 135 | "user": user, 136 | "context": "success", 137 | }) 138 | return 139 | default: 140 | c.JSON(http.StatusInternalServerError, gin.H{}) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /server/user_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/url" 7 | "os" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestInitUser(t *testing.T) { 15 | u := newUser() 16 | u.UserName = DEFAULT_USER_NAME 17 | err := u.LoadByUsername() 18 | 19 | hostname, _ := os.Hostname() 20 | match, _ := comparePassword(hostname, u.Password) 21 | 22 | assert.Equal(t, nil, err, "Default user should exist") 23 | assert.True(t, match, "Default password should match the hostname") 24 | } 25 | 26 | func TestLoginUser(t *testing.T) { 27 | router := setupRouter() 28 | hostname, _ := os.Hostname() 29 | 30 | data := url.Values{} 31 | data.Set("username", DEFAULT_USER_NAME) 32 | data.Set("password", hostname) 33 | 34 | w := httptest.NewRecorder() 35 | req, _ := http.NewRequest("POST", "/user/login", strings.NewReader(data.Encode())) 36 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 37 | router.ServeHTTP(w, req) 38 | 39 | resp := w.Result() 40 | location, _ := resp.Location() 41 | 42 | assert.Equal(t, 302, resp.StatusCode) 43 | assert.Equal(t, "/jobs/view", location.String()) 44 | } 45 | -------------------------------------------------------------------------------- /server/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/subtle" 6 | "encoding/base64" 7 | "errors" 8 | "fmt" 9 | "math" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "github.com/Masterminds/squirrel" 16 | "github.com/rs/xid" 17 | "github.com/spf13/cast" 18 | "golang.org/x/crypto/argon2" 19 | ) 20 | 21 | type APIResponse struct { 22 | GUID xid.ID `json:"guid"` 23 | PID int `json:"pid"` 24 | Msg string `json:"msg"` 25 | Err string `json:"error"` 26 | } 27 | 28 | type passwordConfig struct { 29 | time uint32 30 | memory uint32 31 | threads uint8 32 | keyLen uint32 33 | } 34 | 35 | func fileExists(filename string) bool { 36 | info, err := os.Stat(filename) 37 | if os.IsNotExist(err) { 38 | return false 39 | } 40 | return !info.IsDir() 41 | } 42 | 43 | func fileEmpty(filename string) bool { 44 | if fileExists(filename) { 45 | if info, _ := os.Stat(filename); info.Size() > 0 { 46 | return false 47 | } 48 | } 49 | return true 50 | } 51 | 52 | func formatDuration(timestamp int) string { 53 | if timestamp == 0 { 54 | return "N/A" 55 | } 56 | 57 | t := time.Unix(int64(timestamp-11644473600), 0) 58 | e := time.Since(t) 59 | 60 | d := int(e.Hours()) / 24 61 | h := int(e.Hours()) % 24 62 | m := int(e.Minutes()) % 60 63 | s := int(e.Seconds()) % 60 64 | 65 | return fmt.Sprintf("%d days, %d hrs, %d min, %d sec\n", d, h, m, s) 66 | } 67 | 68 | func generateSecretKey() []byte { 69 | b := make([]byte, 32) 70 | rand.Read(b) 71 | return b 72 | } 73 | 74 | func getVersion() string { 75 | return fmt.Sprintf("%s (rev %s)", BuildVer, BuildRev) 76 | } 77 | 78 | func seq(args ...interface{}) ([]int, error) { 79 | if len(args) < 1 || len(args) > 3 { 80 | return nil, errors.New("invalid number of arguments to Seq") 81 | } 82 | 83 | intArgs := cast.ToIntSlice(args) 84 | if len(intArgs) < 1 || len(intArgs) > 3 { 85 | return nil, errors.New("invalid arguments to Seq") 86 | } 87 | 88 | var inc = 1 89 | var last int 90 | var first = intArgs[0] 91 | 92 | if len(intArgs) == 1 { 93 | last = first 94 | if last == 0 { 95 | return []int{}, nil 96 | } else if last > 0 { 97 | first = 1 98 | } else { 99 | first = -1 100 | inc = -1 101 | } 102 | } else if len(intArgs) == 2 { 103 | last = intArgs[1] 104 | if last < first { 105 | inc = -1 106 | } 107 | } else { 108 | inc = intArgs[1] 109 | last = intArgs[2] 110 | if inc == 0 { 111 | return nil, errors.New("'increment' must not be 0") 112 | } 113 | if first < last && inc < 0 { 114 | return nil, errors.New("'increment' must be > 0") 115 | } 116 | if first > last && inc > 0 { 117 | return nil, errors.New("'increment' must be < 0") 118 | } 119 | } 120 | 121 | // sanity check 122 | if last < -100000 { 123 | return nil, errors.New("size of result exceeds limit") 124 | } 125 | size := ((last - first) / inc) + 1 126 | 127 | // sanity check 128 | if size <= 0 || size > 2000 { 129 | return nil, errors.New("size of result exceeds limit") 130 | } 131 | 132 | seq := make([]int, size) 133 | val := first 134 | for i := 0; ; i++ { 135 | seq[i] = val 136 | val += inc 137 | if (inc < 0 && val < last) || (inc > 0 && val > last) { 138 | break 139 | } 140 | } 141 | 142 | return seq, nil 143 | } 144 | 145 | func formatNumber(num interface{}) string { 146 | x, isFloat := num.(float64) 147 | if !isFloat { 148 | x = float64(num.(int)) 149 | } 150 | 151 | xNum := numberFormat(float64(numberRoundInt(x)), 2, ".", ",") 152 | 153 | if math.Abs(x) < 999.5 { 154 | xNumStr := xNum[:len(xNum)-3] 155 | return string(xNumStr) 156 | } 157 | 158 | // first, remove the .00 then convert to slice 159 | xNumStr := xNum[:len(xNum)-3] 160 | xNumCleaned := strings.Replace(xNumStr, ",", " ", -1) 161 | xNumSlice := strings.Fields(xNumCleaned) 162 | count := len(xNumSlice) - 2 163 | unit := [4]string{"k", "m", "b", "t"} 164 | xPart := unit[count] 165 | 166 | afterDecimal := "" 167 | if xNumSlice[1][0] != 0 { 168 | afterDecimal = "." + string(xNumSlice[1][0]) 169 | } 170 | final := xNumSlice[0] + afterDecimal + xPart 171 | return final 172 | } 173 | 174 | func numberFormat(number float64, decimals int, decPoint, thousandsSep string) string { 175 | if math.IsNaN(number) || math.IsInf(number, 0) { 176 | number = 0 177 | } 178 | 179 | var ret string 180 | var negative bool 181 | 182 | if number < 0 { 183 | number *= -1 184 | negative = true 185 | } 186 | 187 | d, fract := math.Modf(number) 188 | 189 | if decimals <= 0 { 190 | fract = 0 191 | } else { 192 | pow := math.Pow(10, float64(decimals)) 193 | fract = numberRoundPrec(fract*pow, 0) 194 | } 195 | 196 | if thousandsSep == "" { 197 | ret = strconv.FormatFloat(d, 'f', 0, 64) 198 | } else if d >= 1 { 199 | var x float64 200 | for d >= 1 { 201 | d, x = math.Modf(d / 1000) 202 | x = x * 1000 203 | ret = strconv.FormatFloat(x, 'f', 0, 64) + ret 204 | if d >= 1 { 205 | ret = thousandsSep + ret 206 | } 207 | } 208 | } else { 209 | ret = "0" 210 | } 211 | 212 | fracts := strconv.FormatFloat(fract, 'f', 0, 64) 213 | 214 | // "0" pad left 215 | for i := len(fracts); i < decimals; i++ { 216 | fracts = "0" + fracts 217 | } 218 | 219 | ret += decPoint + fracts 220 | 221 | if negative { 222 | ret = "-" + ret 223 | } 224 | return ret 225 | } 226 | 227 | func numberRound(x float64) int { 228 | if math.IsNaN(x) || math.IsInf(x, 0) { 229 | return 0 230 | } 231 | 232 | val := numberRoundPrec(x, 0) 233 | 234 | return int(val) 235 | } 236 | 237 | func numberRoundPrec(x float64, prec int) float64 { 238 | if math.IsNaN(x) || math.IsInf(x, 0) { 239 | return x 240 | } 241 | 242 | sign := 1.0 243 | if x < 0 { 244 | sign = -1 245 | x *= -1 246 | } 247 | 248 | var rounder float64 249 | pow := math.Pow(10, float64(prec)) 250 | intermed := x * pow 251 | _, frac := math.Modf(intermed) 252 | 253 | if frac >= 0.5 { 254 | rounder = math.Ceil(intermed) 255 | } else { 256 | rounder = math.Floor(intermed) 257 | } 258 | 259 | return rounder / pow * sign 260 | } 261 | 262 | func numberRoundInt(input float64) int { 263 | var result float64 264 | 265 | if input < 0 { 266 | result = math.Ceil(input - 0.5) 267 | } else { 268 | result = math.Floor(input + 0.5) 269 | } 270 | 271 | i, _ := math.Modf(result) 272 | 273 | return int(i) 274 | } 275 | 276 | func generatePassword(password string) (string, error) { 277 | var c = &passwordConfig{ 278 | time: 1, 279 | memory: 64 * 1024, 280 | threads: 4, 281 | keyLen: 32, 282 | } 283 | 284 | // Generate a salt. 285 | salt := make([]byte, 16) 286 | if _, err := rand.Read(salt); err != nil { 287 | return "", err 288 | } 289 | 290 | hash := argon2.IDKey([]byte(password), salt, c.time, c.memory, c.threads, c.keyLen) 291 | 292 | // Base64 encode the salt and hashed password. 293 | b64Salt := base64.RawStdEncoding.EncodeToString(salt) 294 | b64Hash := base64.RawStdEncoding.EncodeToString(hash) 295 | 296 | format := "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s" 297 | full := fmt.Sprintf(format, argon2.Version, c.memory, c.time, c.threads, b64Salt, b64Hash) 298 | return full, nil 299 | } 300 | 301 | func comparePassword(password, hash string) (bool, error) { 302 | parts := strings.Split(hash, "$") 303 | if len(parts) != 6 { 304 | return false, errors.New("corrupted password hash") 305 | } 306 | 307 | c := &passwordConfig{} 308 | _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &c.memory, &c.time, &c.threads) 309 | if err != nil { 310 | return false, err 311 | } 312 | 313 | salt, err := base64.RawStdEncoding.DecodeString(parts[4]) 314 | if err != nil { 315 | return false, err 316 | } 317 | 318 | decodedHash, err := base64.RawStdEncoding.DecodeString(parts[5]) 319 | if err != nil { 320 | return false, err 321 | } 322 | c.keyLen = uint32(len(decodedHash)) 323 | 324 | comparisonHash := argon2.IDKey([]byte(password), salt, c.time, c.memory, c.threads, c.keyLen) 325 | 326 | return (subtle.ConstantTimeCompare(decodedHash, comparisonHash) == 1), nil 327 | } 328 | 329 | func totalPages() int { 330 | count := 0 331 | size := 99 332 | 333 | items := squirrel.Select("COUNT(*)").From(TB_NAME_CRASHES) 334 | items.RunWith(db).QueryRow().Scan(&count) 335 | 336 | pages := math.Ceil(float64(count) / float64(size)) 337 | 338 | return int(pages) 339 | } 340 | --------------------------------------------------------------------------------