diff --git a/.gitignore b/.gitignore index 93196b1..4fce7e4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ src *.swp clamrest pyenv +__debug* +.vscode +clamav-rest \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index ccc8939..c94128a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:alpine3.19 as build +FROM golang:alpine3.20 AS build # Update libraries RUN apk update && apk upgrade @@ -8,7 +8,7 @@ WORKDIR /go/src # Build go package ADD . /go/src/clamav-rest/ -RUN cd /go/src/clamav-rest && go mod download github.com/dutchcoders/go-clamd@latest && go mod init clamav-rest && go mod tidy && go mod vendor && go build -v +RUN cd /go/src/clamav-rest && go mod tidy && go build -v FROM alpine:3.21 @@ -31,21 +31,26 @@ ADD ./server.* /etc/ssl/clamav-rest/ # Install ClamAV RUN apk --no-cache add clamav clamav-libunrar \ && mkdir /run/clamav \ - && chown clamav:clamav /run/clamav + && chown clamav:clamav /run/clamav + # Configure clamAV to run in foreground with port 3310 -RUN sed -i 's/^#Foreground .*$/Foreground true/g' /etc/clamav/clamd.conf \ +RUN sed -i 's/^#Foreground .*$/Foreground yes/g' /etc/clamav/clamd.conf \ && sed -i 's/^#TCPSocket .*$/TCPSocket 3310/g' /etc/clamav/clamd.conf \ - && sed -i 's/^#Foreground .*$/Foreground true/g' /etc/clamav/freshclam.conf + && sed -i 's/^#Foreground .*$/Foreground yes/g' /etc/clamav/freshclam.conf RUN freshclam --quiet --no-dns COPY entrypoint.sh /usr/bin/ -RUN mkdir /clamav \ - && chown -R clamav.clamav /clamav \ - && chown -R clamav.clamav /var/log/clamav \ - && chown -R clamav.clamav /run/clamav +RUN mkdir -p /clamav/etc \ + && mkdir -p /clamav/data \ + && mkdir -p /clamav/tmp + +RUN chown -R clamav:clamav /clamav \ + && chown -R clamav:clamav /var/log/clamav \ + && chown -R clamav:clamav /run/clamav + ENV PORT=9000 ENV SSL_PORT=9443 diff --git a/README.md b/README.md index e993b3b..e7383e7 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,12 @@ This is a two in one docker image which runs the open source virus scanner ClamA # Updates -As of October 21 2024, freshclam notifies the correct `.clamd.conf` so that `clamd` is notified about updates and the correct version is returned now. +2025-01-08: [PR 50](https://github.com/ajilach/clamav-rest/pull/50) integrated which now provides a new `/v2` endpoint returning more scan result information: status, description, http status and a list of scanned files. See the PR for more details. The old `/scan` endpoint is now considered deprecated. Also, a file size scan limit has been added which can be configured through the `MAX_FILE_SIZE` environment variable. + +2024-10-21: freshclam notifies the correct `.clamd.conf` so that `clamd` is notified about updates and the correct version is returned now. This is an additional fix to the latest fix from October 15 2024 which was not working. Thanks to [christianbumann](https://github.com/christianbumann) and [arizon-dread](https://github.com/arizon-dread). -As of October 15 2024, ClamAV was thought to handle database updates correctly thanks to [christianbumann](https://github.com/christianbumann). It turned out that this was not the case. +2024-10-15: ClamAV was thought to handle database updates correctly thanks to [christianbumann](https://github.com/christianbumann). It turned out that this was not the case. As of May 2024, the releases are built for multiple architectures thanks to efforts from [kcirtapfromspace](https://github.com/kcirtapfromspace) and support non-root read-only deployments thanks to [robaca](https://github.com/robaca). @@ -59,9 +61,10 @@ docker run -p 9000:9000 -p 9443:9443 -itd --name clamav-rest ajilaag/clamav-rest Test that service detects common test virus signature: -**HTTP** +**HTTP:** + ```bash -$ curl -i -F "file=@eicar.com.txt" http://localhost:9000/scan +$ curl -i -F "file=@eicar.com.txt" http://localhost:9000/v2/scan HTTP/1.1 100 Continue HTTP/1.1 406 Not Acceptable @@ -69,12 +72,13 @@ Content-Type: application/json; charset=utf-8 Date: Mon, 28 Aug 2017 20:22:34 GMT Content-Length: 56 -{ "Status": "FOUND", "Description": "Eicar-Test-Signature" } +[{ "Status": "FOUND", "Description": "Eicar-Test-Signature","FileName":"eicar.com.txt"}] ``` -**HTTPS** +**HTTPS:** + ```bash -$ curl -i -k -F "file=@eicar.com.txt" https://localhost:9443/scan +$ curl -i -k -F "file=@eicar.com.txt" https://localhost:9443/v2/scan HTTP/1.1 100 Continue HTTP/1.1 406 Not Acceptable @@ -82,14 +86,15 @@ Content-Type: application/json; charset=utf-8 Date: Mon, 28 Aug 2017 20:22:34 GMT Content-Length: 56 -{ "Status": "FOUND", "Description": "Eicar-Test-Signature" } +[{ "Status": "FOUND", "Description": "Eicar-Test-Signature","FileName":"eicar.com.txt"}] ``` Test that service returns 200 for clean file: -**HTTP** +**HTTP:** + ```bash -$ curl -i -F "file=@clamrest.go" http://localhost:9000/scan +$ curl -i -F "file=@clamrest.go" http://localhost:9000/v2/scan HTTP/1.1 100 Continue @@ -98,11 +103,12 @@ Content-Type: application/json; charset=utf-8 Date: Mon, 28 Aug 2017 20:23:16 GMT Content-Length: 33 -{ "Status": "OK", "Description": "" } +[{ "Status": "OK", "Description": "","FileName":"clamrest.go"}] ``` -**HTTPS** +**HTTPS:** + ```bash -$ curl -i -k -F "file=@clamrest.go" https://localhost:9443/scan +$ curl -i -k -F "file=@clamrest.go" https://localhost:9443/v2/scan HTTP/1.1 100 Continue @@ -111,7 +117,7 @@ Content-Type: application/json; charset=utf-8 Date: Mon, 28 Aug 2017 20:23:16 GMT Content-Length: 33 -{ "Status": "OK", "Description": "" } +[{ "Status": "OK", "Description": "","FileName":"clamrest.go"}] ``` ## Status Codes @@ -119,6 +125,8 @@ Content-Length: 33 - 400 - ClamAV returned general error for file - 406 - INFECTED - 412 - unable to parse file +- 413 - request entity too large, the file exceeds the scannable limit. Set MAX_FILE_SIZE to scan larger files +- 422 - filename is missing in MimePart - 501 - unknown request # Configuration @@ -172,24 +180,28 @@ clamscan --database=/clamav/data --version [Prometheus metrics](https://prometheus.io/docs/guides/go-application/) were implemented, which can be retrieved as follows -**HTTP**: +**HTTP:** curl http://localhost:9000/metrics **HTTPS:** curl https://localhost:9443/metrics -# Developing +# Development -Source Code can be found here: https://github.com/ajilach/clamav-rest +Source code can be found here: https://github.com/ajilach/clamav-rest Build golang (linux) binary and docker image: ```bash # env GOOS=linux GOARCH=amd64 go build -docker build . -t clamav-go-rest -docker run -p 9000:9000 -p 9443:9443 -itd --name clamav-rest clamav-go-rest +docker build . -t clamav-rest +docker run -p 9000:9000 -p 9443:9443 -itd --name clamav-rest clamav-rest ``` +# History + +This work is based on the awesome work done by [o20ne/clamav-rest](https://github.com/o20ne/clamav-rest) which is based on [niilo/clamav-rest](https://github.com/niilo/clamav-rest) which is based on the original code from [osterzel/clamav-rest](https://github.com/osterzel/clamav-rest). + # References * https://www.clamav.net diff --git a/centos.Dockerfile b/centos.Dockerfile index 0465813..56cdabd 100644 --- a/centos.Dockerfile +++ b/centos.Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/centos/centos:stream8 as build +FROM quay.io/centos/centos:stream9 as build # Set timezone to Europe/Zurich ENV TZ=Europe/Zurich @@ -16,9 +16,9 @@ ENV GOPATH=/go \ # Build go package ADD . /go/src/clamav-rest/ -RUN cd /go/src/clamav-rest && go mod download github.com/dutchcoders/go-clamd@latest && go mod init clamav-rest && go mod tidy && go mod vendor && go build -v +RUN cd /go/src/clamav-rest && go mod tidy && go build -v -FROM quay.io/centos/centos:stream8 +FROM quay.io/centos/centos:stream9 # Copy compiled clamav-rest binary from build container to production container COPY --from=build /go/src/clamav-rest/clamav-rest /usr/bin/ @@ -26,7 +26,7 @@ COPY --from=build /go/src/clamav-rest/clamav-rest /usr/bin/ # Install ClamAV RUN dnf -y update \ && dnf install -y epel-release \ - && dnf install -y clamav-server clamav-data clamav-update clamav-filesystem clamav clamav-scanner-systemd clamav-devel clamav-lib clamav-server-systemd \ + && dnf install -y clamav-server clamav-data clamav-update clamav-filesystem clamav clamav-scanner-systemd clamav-devel clamav-lib clamav-server-systemd nc \ && mkdir /run/clamav \ && chown clamscan:clamscan /run/clamav \ # Clean @@ -44,7 +44,15 @@ RUN freshclam --quiet --no-dns ADD ./server.* /etc/ssl/clamav-rest/ COPY entrypoint.sh /usr/bin/ -RUN mkdir /etc/clamav/ && ln -s /etc/clamd.d/scan.conf /etc/clamav/clamd.conf + +# Create folders for clamav so it matches what happens in entrypoint.sh +RUN install -d -m 0775 -oclamupdate -groot /var/log/clamav /etc/clamav /clamav /clamav/etc /clamav/data /clamav/tmp \ + && cp /etc/clamd.d/scan.conf /etc/clamav/clamd.conf \ + && cp /etc/freshclam.conf /etc/clamav/freshclam.conf \ + && chown clamupdate:root /etc/clamav/freshclam.conf + +# On CentOS, clamupdate is the user. +USER clamupdate EXPOSE 9000 EXPOSE 9443 diff --git a/clamrest.go b/clamrest.go index 89f37f5..c9afb07 100644 --- a/clamrest.go +++ b/clamrest.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "log" "net/http" "os" @@ -19,8 +18,13 @@ import ( var opts map[string]string +var noOfFoundViruses = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "no_of_found_viruses", + Help: "The total number of found viruses", +}) + func init() { - log.SetOutput(ioutil.Discard) + log.SetOutput(io.Discard) } func clamversion(w http.ResponseWriter, r *http.Request) { @@ -41,7 +45,7 @@ func clamversion(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") for version_string := range version { if strings.HasPrefix(version_string.Raw, "ClamAV ") { - version_values := strings.Split(strings.Replace(version_string.Raw, "ClamAV ", "", 1),"/") + version_values := strings.Split(strings.Replace(version_string.Raw, "ClamAV ", "", 1), "/") respJson := fmt.Sprintf("{ \"Clamav\": \"%s\" }", version_values[0]) if len(version_values) == 3 { respJson = fmt.Sprintf("{ \"Clamav\": \"%s\", \"Signature\": \"%s\" , \"Signature_date\": \"%s\" }", version_values[0], version_values[1], version_values[2]) @@ -100,8 +104,10 @@ func scanPathHandler(w http.ResponseWriter, r *http.Request) { } var scanResults []*clamd.ScanResult - for responseItem := range response { + if responseItem.Status == clamd.RES_FOUND { + noOfFoundViruses.Inc() + } scanResults = append(scanResults, responseItem) } @@ -114,8 +120,21 @@ func scanPathHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(resJson)) } -//This is where the action happens. +func v2ScanHandler(w http.ResponseWriter, r *http.Request) { + scanner(w, r, 2) +} + +// old endpoint version, set deprecation header to indicate usage of the new /v2/scan func scanHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Deprecation", "version=v1") + v2url := fmt.Sprintf("%s%s/v2/scan", string(r.URL.Scheme), r.Host) + w.Header().Add("Link", fmt.Sprintf("%v; rel=successor-version", v2url)) + + scanner(w, r, 1) +} + +// This is where the action happens. +func scanner(w http.ResponseWriter, r *http.Request, version int) { switch r.Method { //POST takes the uploaded file(s) and saves it to disk. case "POST": @@ -127,8 +146,8 @@ func scanHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - //copy each part to destination. + resp := []scanResponse{} for { part, err := reader.NextPart() if err == io.EOF { @@ -137,37 +156,112 @@ func scanHandler(w http.ResponseWriter, r *http.Request) { //if part.FileName() is empty, skip this iteration. if part.FileName() == "" { + if version == 2 { + fileResp := scanResponse{Status: "ERROR", Description: "MimePart FileName missing", httpStatus: 422} + resp = append(resp, fileResp) + fmt.Printf("%v Not scanning, MimePart FileName not supplied\n", time.Now().Format(time.RFC3339)) + } continue } - fmt.Printf(time.Now().Format(time.RFC3339) + " Started scanning: " + part.FileName() + "\n") + fmt.Printf("%v Started scanning: %v\n", time.Now().Format(time.RFC3339), part.FileName()) var abort chan bool response, err := c.ScanStream(part, abort) + if err != nil { + //error occurred, response is nil, create a custom response and send it on the channel to handle it together with the other errors. + response = make(chan *clamd.ScanResult) + scanErrResult := &clamd.ScanResult{Status: clamd.RES_PARSE_ERROR, Description: "File size limit exceeded"} + go func() { + response <- scanErrResult + close(response) + fmt.Printf("%v Clamd returned an error, probably a too large file as input (causing broken pipe and closed connection) %v\n", time.Now().Format(time.RFC3339), err) + //The underlying service closes the connection if the file is to large, logging output + //We never receive the clamd output of `^INSTREAM: Size limit reached` up here, just a closed connection. + }() + + } for s := range response { w.Header().Set("Content-Type", "application/json; charset=utf-8") - respJson := fmt.Sprintf("{ \"Status\": \"%s\", \"Description\": \"%s\" }", s.Status, s.Description) - switch s.Status { - case clamd.RES_OK: - w.WriteHeader(http.StatusOK) - case clamd.RES_FOUND: - w.WriteHeader(http.StatusNotAcceptable) - case clamd.RES_ERROR: - w.WriteHeader(http.StatusBadRequest) - case clamd.RES_PARSE_ERROR: - w.WriteHeader(http.StatusPreconditionFailed) - default: - w.WriteHeader(http.StatusNotImplemented) + eachResp := scanResponse{Status: s.Status, Description: s.Description} + if version == 2 { + eachResp.FileName = part.FileName() + fmt.Printf("scanned file %v", part.FileName()) } - fmt.Fprint(w, respJson) - fmt.Printf(time.Now().Format(time.RFC3339)+" Scan result for: %v, %v\n", part.FileName(), s) + //Set each possible status and then send the most appropriate one + eachResp.httpStatus = getHttpStatusByClamStatus(s) + resp = append(resp, eachResp) + fmt.Printf("%v Scan result for: %v, %v\n", time.Now().Format(time.RFC3339), part.FileName(), s) } - fmt.Printf(time.Now().Format(time.RFC3339) + " Finished scanning: " + part.FileName() + "\n") + fmt.Printf("%v Finished scanning: %v\n", time.Now().Format(time.RFC3339), part.FileName()) } + w.WriteHeader(getResponseStatus(resp)) + if version == 2 { + jsonRes, jErr := json.Marshal(resp) + if jErr != nil { + fmt.Printf("Error marshalling json, %v\n", jErr) + } + fmt.Fprint(w, string(jsonRes)) + } else { + for _, v := range resp { + jsonRes, jErr := json.Marshal(v) + if jErr != nil { + fmt.Printf("Error marshalling json, %v\n", jErr) + } + fmt.Fprint(w, string(jsonRes)) + } + } + default: w.WriteHeader(http.StatusMethodNotAllowed) } } +func getHttpStatusByClamStatus(result *clamd.ScanResult) int { + switch result.Status { + case clamd.RES_OK: + return http.StatusOK //200 + case clamd.RES_FOUND: + fmt.Printf("%v Virus FOUND\n", time.Now().Format(time.RFC3339)) + return http.StatusNotAcceptable //406 + case clamd.RES_ERROR: + return http.StatusBadRequest //400 + case clamd.RES_PARSE_ERROR: + if result.Description == "File size limit exceeded" { + return http.StatusRequestEntityTooLarge //413 + } else { + return http.StatusPreconditionFailed //412 + } + default: + return http.StatusNotImplemented //501 + } +} + +// this func returns 406 if one file contains a virus +func getResponseStatus(responses []scanResponse) int { + result := 200 + for _, r := range responses { + switch r.httpStatus { + case 406: + //uptick the prometheus counter for detected viruses. + noOfFoundViruses.Inc() + //early return if virus is found + return 406 + case 400: + result = 400 + case 412: + result = 412 + case 413: + result = 413 + case 422: + result = 422 + case 501: + result = 501 + } + } + + return result +} + func scanHandlerBody(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { w.WriteHeader(http.StatusMethodNotAllowed) @@ -176,27 +270,34 @@ func scanHandlerBody(w http.ResponseWriter, r *http.Request) { c := clamd.NewClamd(opts["CLAMD_PORT"]) - fmt.Printf(time.Now().Format(time.RFC3339) + " Started scanning plain body\n") + fmt.Printf("%v Started scanning plain body\n", time.Now().Format(time.RFC3339)) var abort chan bool defer r.Body.Close() - response, _ := c.ScanStream(r.Body, abort) - for s := range response { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - respJson := fmt.Sprintf("{ Status: %q, Description: %q }", s.Status, s.Description) - switch s.Status { - case clamd.RES_OK: - w.WriteHeader(http.StatusOK) - case clamd.RES_FOUND: - w.WriteHeader(http.StatusNotAcceptable) - case clamd.RES_ERROR: - w.WriteHeader(http.StatusBadRequest) - case clamd.RES_PARSE_ERROR: - w.WriteHeader(http.StatusPreconditionFailed) - default: - w.WriteHeader(http.StatusNotImplemented) + response, err := c.ScanStream(r.Body, abort) + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if err != nil { + w.WriteHeader(http.StatusRequestEntityTooLarge) + resp := scanResponse{Status: clamd.RES_PARSE_ERROR, Description: "File size limit exceeded"} + fmt.Printf("%v Clamd returned error, broken pipe and closed connection can indicate too large file, %v", time.Now().Format(time.RFC3339), err) + jsonResp, jErr := json.Marshal(resp) + if jErr != nil { + fmt.Printf("%v Error marshalling json, %v", time.Now().Format(time.RFC3339), jErr) } - fmt.Fprint(w, respJson) - fmt.Printf(time.Now().Format(time.RFC3339)+" Scan result for plain body: %v\n", s) + fmt.Fprint(w, string(jsonResp)) + return + } + for s := range response { + + resp := scanResponse{Status: s.Status, Description: s.Description} + //respJson := fmt.Sprintf("{ Status: %q, Description: %q }", s.Status, s.Description) + resp.httpStatus = getHttpStatusByClamStatus(s) + + resps := []scanResponse{} + resps = append(resps, resp) + w.WriteHeader(getResponseStatus(resps)) + fmt.Fprint(w, resp) + fmt.Printf("%v Scan result for plain body: %v\n", time.Now().Format(time.RFC3339), s) } } @@ -231,6 +332,7 @@ func main() { reg.MustRegister(collectors.NewGoCollector( collectors.WithGoCollections(collectors.GoRuntimeMemStatsCollection | collectors.GoRuntimeMetricsCollection), )) + reg.MustRegister(noOfFoundViruses) for _, e := range os.Environ() { pair := strings.Split(e, "=") @@ -248,9 +350,10 @@ func main() { fmt.Printf("Connected to clamd on %v\n", opts["CLAMD_PORT"]) http.HandleFunc("/scan", scanHandler) + http.HandleFunc("/v2/scan", v2ScanHandler) http.HandleFunc("/scanPath", scanPathHandler) http.HandleFunc("/scanHandlerBody", scanHandlerBody) - http.HandleFunc("/version", clamversion) + http.HandleFunc("/version", clamversion) http.HandleFunc("/", home) // Prometheus metrics diff --git a/entrypoint.sh b/entrypoint.sh index 4a246c5..40dcc44 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,13 +1,11 @@ #!/bin/bash -mkdir -p /clamav/etc -mkdir -p /clamav/data -mkdir -p /clamav/tmp cp /etc/clamav/* /clamav/etc/ # Replace values in freshclam.conf sed -i 's/^#\?NotifyClamd .*$/NotifyClamd \/clamav\/etc\/clamd.conf/g' /clamav/etc/freshclam.conf sed -i 's/^#DatabaseDirectory .*$/DatabaseDirectory \/clamav\/data/g' /clamav/etc/freshclam.conf +sed -i 's/^#\?NotifyClamd .*$/NotifyClamd \/clamav\/etc\/clamd.conf/g' /clamav/etc/freshclam.conf sed -i 's/^#TemporaryDirectory .*$/TemporaryDirectory \/clamav\/tmp/g' /clamav/etc/clamd.conf sed -i 's/^#DatabaseDirectory .*$/DatabaseDirectory \/clamav\/data/g' /clamav/etc/clamd.conf diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..20154c6 --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module github.com/ajilach/clamav-rest + +go 1.22.4 + +require ( + github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e + github.com/prometheus/client_golang v1.19.1 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + golang.org/x/sys v0.17.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e2e9147 --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e h1:rcHHSQqzCgvlwP0I/fQ8rQMn/MpHE5gWSLdtpxtP6KQ= +github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e/go.mod h1:Byz7q8MSzSPkouskHJhX0er2mZY/m0Vj5bMeMCkkyY4= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= diff --git a/scanResponse.go b/scanResponse.go new file mode 100644 index 0000000..d386080 --- /dev/null +++ b/scanResponse.go @@ -0,0 +1,8 @@ +package main + +type scanResponse struct { + Status string `json:"Status"` + Description string `json:"Description"` + FileName string `json:"FileName,omitempty"` + httpStatus int `json:"-"` +} diff --git a/vendor/github.com/dutchcoders/go-clamd/.gitignore b/vendor/github.com/dutchcoders/go-clamd/.gitignore deleted file mode 100644 index daf913b..0000000 --- a/vendor/github.com/dutchcoders/go-clamd/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof diff --git a/vendor/github.com/dutchcoders/go-clamd/.travis.yml b/vendor/github.com/dutchcoders/go-clamd/.travis.yml deleted file mode 100644 index 212857d..0000000 --- a/vendor/github.com/dutchcoders/go-clamd/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: go -go: - - 1.1 - - 1.2 - - 1.3 - - release - - tip - -script: - - go test -v ./... diff --git a/vendor/github.com/dutchcoders/go-clamd/LICENSE b/vendor/github.com/dutchcoders/go-clamd/LICENSE deleted file mode 100644 index e85f3f1..0000000 --- a/vendor/github.com/dutchcoders/go-clamd/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 dutchcoders - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/vendor/github.com/dutchcoders/go-clamd/README.md b/vendor/github.com/dutchcoders/go-clamd/README.md deleted file mode 100644 index f043bbf..0000000 --- a/vendor/github.com/dutchcoders/go-clamd/README.md +++ /dev/null @@ -1,35 +0,0 @@ -go-clamd -======== - -Interface to clamd (clamav daemon). You can use go-clamd to implement virus detection capabilities to your application. - -[![GoDoc](https://godoc.org/github.com/dutchcoders/go-clamd?status.svg)](https://godoc.org/github.com/dutchcoders/go-clamd) -[![Build Status](https://travis-ci.org/dutchcoders/go-clamd.svg?branch=master)](https://travis-ci.org/dutchcoders/go-clamd) - -## Examples - -``` -c := clamd.NewClamd("/tmp/clamd.socket") - -reader := bytes.NewReader(clamd.EICAR) -response, err := c.ScanStream(reader, make(chan bool)) - -for s := range response { - fmt.Printf("%v %v\n", s, err) -} -``` - -## Contributions - -Contributions are welcome. - -## Creators - -**Remco Verhoef** -- - -- - -## Copyright and license - -Code and documentation copyright 2011-2014 Remco Verhoef. Code released under [the MIT license](LICENSE). diff --git a/vendor/github.com/dutchcoders/go-clamd/clamd.go b/vendor/github.com/dutchcoders/go-clamd/clamd.go deleted file mode 100644 index 5199f63..0000000 --- a/vendor/github.com/dutchcoders/go-clamd/clamd.go +++ /dev/null @@ -1,311 +0,0 @@ -/* -Open Source Initiative OSI - The MIT License (MIT):Licensing - -The MIT License (MIT) -Copyright (c) 2013 DutchCoders - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -package clamd - -import ( - "errors" - "fmt" - "io" - "net/url" - "strings" -) - -const ( - RES_OK = "OK" - RES_FOUND = "FOUND" - RES_ERROR = "ERROR" - RES_PARSE_ERROR = "PARSE ERROR" -) - -type Clamd struct { - address string -} - -type Stats struct { - Pools string - State string - Threads string - Memstats string - Queue string -} - -type ScanResult struct { - Raw string - Description string - Path string - Hash string - Size int - Status string -} - -var EICAR = []byte(`X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*`) - -func (c *Clamd) newConnection() (conn *CLAMDConn, err error) { - - var u *url.URL - - if u, err = url.Parse(c.address); err != nil { - return - } - - switch u.Scheme { - case "tcp": - conn, err = newCLAMDTcpConn(u.Host) - case "unix": - conn, err = newCLAMDUnixConn(u.Path) - default: - conn, err = newCLAMDUnixConn(c.address) - } - - return -} - -func (c *Clamd) simpleCommand(command string) (chan *ScanResult, error) { - conn, err := c.newConnection() - if err != nil { - return nil, err - } - - err = conn.sendCommand(command) - if err != nil { - return nil, err - } - - ch, wg, err := conn.readResponse() - - go func() { - wg.Wait() - conn.Close() - }() - - return ch, err -} - -/* -Check the daemon's state (should reply with PONG). -*/ -func (c *Clamd) Ping() error { - ch, err := c.simpleCommand("PING") - if err != nil { - return err - } - - select { - case s := (<-ch): - switch s.Raw { - case "PONG": - return nil - default: - return errors.New(fmt.Sprintf("Invalid response, got %s.", s)) - } - } - - return nil -} - -/* -Print program and database versions. -*/ -func (c *Clamd) Version() (chan *ScanResult, error) { - dataArrays, err := c.simpleCommand("VERSION") - return dataArrays, err -} - -/* -On this command clamd provides statistics about the scan queue, contents of scan -queue, and memory usage. The exact reply format is subject to changes in future -releases. -*/ -func (c *Clamd) Stats() (*Stats, error) { - ch, err := c.simpleCommand("STATS") - if err != nil { - return nil, err - } - - stats := &Stats{} - - for s := range ch { - if strings.HasPrefix(s.Raw, "POOLS") { - stats.Pools = strings.Trim(s.Raw[6:], " ") - } else if strings.HasPrefix(s.Raw, "STATE") { - stats.State = s.Raw - } else if strings.HasPrefix(s.Raw, "THREADS") { - stats.Threads = s.Raw - } else if strings.HasPrefix(s.Raw, "QUEUE") { - stats.Queue = s.Raw - } else if strings.HasPrefix(s.Raw, "MEMSTATS") { - stats.Memstats = s.Raw - } else if strings.HasPrefix(s.Raw, "END") { - } else { - // return nil, errors.New(fmt.Sprintf("Unknown response, got %s.", s)) - } - } - - return stats, nil -} - -/* -Reload the databases. -*/ -func (c *Clamd) Reload() error { - ch, err := c.simpleCommand("RELOAD") - if err != nil { - return err - } - - select { - case s := (<-ch): - switch s.Raw { - case "RELOADING": - return nil - default: - return errors.New(fmt.Sprintf("Invalid response, got %s.", s)) - } - } - - return nil -} - -func (c *Clamd) Shutdown() error { - _, err := c.simpleCommand("SHUTDOWN") - if err != nil { - return err - } - - return err -} - -/* -Scan file or directory (recursively) with archive support enabled (a full path is -required). -*/ -func (c *Clamd) ScanFile(path string) (chan *ScanResult, error) { - command := fmt.Sprintf("SCAN %s", path) - ch, err := c.simpleCommand(command) - return ch, err -} - -/* -Scan file or directory (recursively) with archive and special file support disabled -(a full path is required). -*/ -func (c *Clamd) RawScanFile(path string) (chan *ScanResult, error) { - command := fmt.Sprintf("RAWSCAN %s", path) - ch, err := c.simpleCommand(command) - return ch, err -} - -/* -Scan file in a standard way or scan directory (recursively) using multiple threads -(to make the scanning faster on SMP machines). -*/ -func (c *Clamd) MultiScanFile(path string) (chan *ScanResult, error) { - command := fmt.Sprintf("MULTISCAN %s", path) - ch, err := c.simpleCommand(command) - return ch, err -} - -/* -Scan file or directory (recursively) with archive support enabled and don’t stop -the scanning when a virus is found. -*/ -func (c *Clamd) ContScanFile(path string) (chan *ScanResult, error) { - command := fmt.Sprintf("CONTSCAN %s", path) - ch, err := c.simpleCommand(command) - return ch, err -} - -/* -Scan file or directory (recursively) with archive support enabled and don’t stop -the scanning when a virus is found. -*/ -func (c *Clamd) AllMatchScanFile(path string) (chan *ScanResult, error) { - command := fmt.Sprintf("ALLMATCHSCAN %s", path) - ch, err := c.simpleCommand(command) - return ch, err -} - -/* -Scan a stream of data. The stream is sent to clamd in chunks, after INSTREAM, -on the same socket on which the command was sent. This avoids the overhead -of establishing new TCP connections and problems with NAT. The format of the -chunk is: where is the size of the following data in -bytes expressed as a 4 byte unsigned integer in network byte order and is -the actual chunk. Streaming is terminated by sending a zero-length chunk. Note: -do not exceed StreamMaxLength as defined in clamd.conf, otherwise clamd will -reply with INSTREAM size limit exceeded and close the connection -*/ -func (c *Clamd) ScanStream(r io.Reader, abort chan bool) (chan *ScanResult, error) { - conn, err := c.newConnection() - if err != nil { - return nil, err - } - - go func() { - for { - _, allowRunning := <-abort - if !allowRunning { - break - } - } - conn.Close() - }() - - conn.sendCommand("INSTREAM") - - for { - buf := make([]byte, CHUNK_SIZE) - - nr, err := r.Read(buf) - if nr > 0 { - conn.sendChunk(buf[0:nr]) - } - - if err != nil { - break - } - - } - - err = conn.sendEOF() - if err != nil { - return nil, err - } - - ch, wg, err := conn.readResponse() - - go func() { - wg.Wait() - conn.Close() - }() - - return ch, nil -} - -func NewClamd(address string) *Clamd { - clamd := &Clamd{address: address} - return clamd -} diff --git a/vendor/github.com/dutchcoders/go-clamd/conn.go b/vendor/github.com/dutchcoders/go-clamd/conn.go deleted file mode 100644 index 5c9f7f9..0000000 --- a/vendor/github.com/dutchcoders/go-clamd/conn.go +++ /dev/null @@ -1,178 +0,0 @@ -/* -Open Source Initiative OSI - The MIT License (MIT):Licensing - -The MIT License (MIT) -Copyright (c) 2013 DutchCoders - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -package clamd - -import ( - "bufio" - "fmt" - "io" - "net" - "regexp" - "strconv" - "strings" - "sync" - "time" -) - -const CHUNK_SIZE = 1024 -const TCP_TIMEOUT = time.Second * 2 - -var resultRegex = regexp.MustCompile( - `^(?P[^:]+): ((?P[^:]+)(\((?P([^:]+)):(?P\d+)\))? )?(?PFOUND|ERROR|OK)$`, -) - -type CLAMDConn struct { - net.Conn -} - -func (conn *CLAMDConn) sendCommand(command string) error { - commandBytes := []byte(fmt.Sprintf("n%s\n", command)) - - _, err := conn.Write(commandBytes) - return err -} - -func (conn *CLAMDConn) sendEOF() error { - _, err := conn.Write([]byte{0, 0, 0, 0}) - return err -} - -func (conn *CLAMDConn) sendChunk(data []byte) error { - var buf [4]byte - lenData := len(data) - buf[0] = byte(lenData >> 24) - buf[1] = byte(lenData >> 16) - buf[2] = byte(lenData >> 8) - buf[3] = byte(lenData >> 0) - - a := buf - - b := make([]byte, len(a)) - for i := range a { - b[i] = a[i] - } - - conn.Write(b) - - _, err := conn.Write(data) - return err -} - -func (c *CLAMDConn) readResponse() (chan *ScanResult, *sync.WaitGroup, error) { - var wg sync.WaitGroup - - wg.Add(1) - reader := bufio.NewReader(c) - ch := make(chan *ScanResult) - - go func() { - defer func() { - close(ch) - wg.Done() - }() - - for { - line, err := reader.ReadString('\n') - if err == io.EOF { - return - } - - if err != nil { - return - } - - line = strings.TrimRight(line, " \t\r\n") - ch <- parseResult(line) - } - }() - - return ch, &wg, nil -} - -func parseResult(line string) *ScanResult { - res := &ScanResult{} - res.Raw = line - - matches := resultRegex.FindStringSubmatch(line) - if len(matches) == 0 { - res.Description = "Regex had no matches" - res.Status = RES_PARSE_ERROR - return res - } - - for i, name := range resultRegex.SubexpNames() { - switch name { - case "path": - res.Path = matches[i] - case "desc": - res.Description = matches[i] - case "virhash": - res.Hash = matches[i] - case "virsize": - i, err := strconv.Atoi(matches[i]) - if err == nil { - res.Size = i - } - case "status": - switch matches[i] { - case RES_OK: - case RES_FOUND: - case RES_ERROR: - break - default: - res.Description = "Invalid status field: " + matches[i] - res.Status = RES_PARSE_ERROR - return res - } - res.Status = matches[i] - } - } - - return res -} - -func newCLAMDTcpConn(address string) (*CLAMDConn, error) { - conn, err := net.DialTimeout("tcp", address, TCP_TIMEOUT) - - if err != nil { - if nerr, isOk := err.(net.Error); isOk && nerr.Timeout() { - return nil, nerr - } - - return nil, err - } - - return &CLAMDConn{Conn: conn}, err -} - -func newCLAMDUnixConn(address string) (*CLAMDConn, error) { - conn, err := net.Dial("unix", address) - if err != nil { - return nil, err - } - - return &CLAMDConn{Conn: conn}, err -} diff --git a/vendor/github.com/dutchcoders/go-clamd/examples/main.go b/vendor/github.com/dutchcoders/go-clamd/examples/main.go deleted file mode 100644 index 1b4ccf2..0000000 --- a/vendor/github.com/dutchcoders/go-clamd/examples/main.go +++ /dev/null @@ -1,72 +0,0 @@ -/* -Open Source Initiative OSI - The MIT License (MIT):Licensing - -The MIT License (MIT) -Copyright (c) 2013 DutchCoders - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -package main - -import ( - _ "bytes" - "fmt" - "github.com/dutchcoders/go-clamd" -) - -func main() { - fmt.Println("Made with <3 DutchCoders") - - c := clamd.NewClamd("/tmp/clamd.socket") - _ = c - - /* - reader := bytes.NewReader(clamd.EICAR) - response, err := c.ScanStream(reader) - - for s := range response { - fmt.Printf("%v %v\n", s, err) - } - - response, err = c.ScanFile(".") - - for s := range response { - fmt.Printf("%v %v\n", s, err) - } - - response, err = c.Version() - - for s := range response { - fmt.Printf("%v %v\n", s, err) - } - */ - - err := c.Ping() - fmt.Printf("Ping: %v\n", err) - - stats, err := c.Stats() - fmt.Printf("%v %v\n", stats, err) - - err = c.Reload() - fmt.Printf("Reload: %v\n", err) - - // response, err = c.Shutdown() - // fmt.Println(response) -}