Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unify response headers from different scan methods & new ScanFile endpoint #51

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Status Codes](#status-codes)
- [Additional endpoints](#additional-endpoints)
- [Configuration](#configuration)
- [Environment Variables](#environment-variables)
- [Networking](#networking)
Expand Down Expand Up @@ -116,6 +117,44 @@ Content-Length: 33
- 412 - unable to parse file
- 501 - unknown request


# Additional endpoints

## Print version and signature dates

```bash
$ curl -i http://localhost:9000/version

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Fri, 12 Jul 2024 12:21:59 GMT
Content-Length: 90

{ "Clamav": "1.2.2", "Signature": "27333" , "Signature_date": "Thu Jul 11 10:35:59 2024" }
```

## Scan files on the file system (should be mounted into the container of course)

Both endpoints can handle files and paths:

```bash
$ curl -i http://localhost:9000/scanPath?path=/scandirectory/eicar.txt

(uses clamd ALLMATCHSCAN) or

$ curl -i http://localhost:9000/scanFile?path=/scandirectory/eicar.txt

(uses clamd SCAN)

HTTP/1.1 406 Not Acceptable
Content-Type: application/json; charset=utf-8
Date: Fri, 12 Jul 2024 12:27:44 GMT
Content-Length: 60

{ "Status": "FOUND", "Description": "Win.Test.EICAR_HDB-1" }
```


# Configuration

## Environment Variables
Expand Down
93 changes: 56 additions & 37 deletions clamrest.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,56 @@ func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, string(resJson))
}

func handleScanResults(scanResults chan* clamd.ScanResult, path string, w http.ResponseWriter) {
for s := range scanResults {
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)
}
fmt.Fprint(w, respJson)
fmt.Printf(time.Now().Format(time.RFC3339)+" Scan result for: %v, %v\n", path, s)
}
}

func scanFileHandler(w http.ResponseWriter, r *http.Request) {
paths, ok := r.URL.Query()["path"]
if !ok || len(paths[0]) < 1 {
log.Println("Url Param 'path' is missing")
return
}

path := paths[0]

c := clamd.NewClamd(opts["CLAMD_PORT"])
fmt.Printf(time.Now().Format(time.RFC3339) + " Started scanning: " + path + "\n")
response, err := c.ScanFile(path)

if err != nil {
errJson, eErr := json.Marshal(err)
if eErr != nil {
fmt.Println(eErr)
return
}
fmt.Fprint(w, string(errJson))
return
}

handleScanResults(response, path, w)

fmt.Printf(time.Now().Format(time.RFC3339) + " Finished scanning: " + path + "\n")
}


func scanPathHandler(w http.ResponseWriter, r *http.Request) {
paths, ok := r.URL.Query()["path"]
if !ok || len(paths[0]) < 1 {
Expand Down Expand Up @@ -114,7 +164,6 @@ func scanPathHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, string(resJson))
}

//This is where the action happens.
func scanHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
//POST takes the uploaded file(s) and saves it to disk.
Expand Down Expand Up @@ -143,24 +192,9 @@ func scanHandler(w http.ResponseWriter, r *http.Request) {
fmt.Printf(time.Now().Format(time.RFC3339) + " Started scanning: " + part.FileName() + "\n")
var abort chan bool
response, err := c.ScanStream(part, abort)
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)
}
fmt.Fprint(w, respJson)
fmt.Printf(time.Now().Format(time.RFC3339)+" Scan result for: %v, %v\n", part.FileName(), s)
}

handleScanResults(response, part.FileName(), w)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will falsely return a 200 OK if a FormPost with multiple files are used to call the /scan endpoint and the first file is clean, but the second one contains a virus, once you w.WriteHeader(http.Status), the response status will not change if you write it again. Maybe you are aware of this, considering your comment about limitations. This PR fixes that bug

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right - this is of course a security hole. Hope the maintainer merges your PR... then we could add more endpoints and get rid of some code duplications.

Copy link
Collaborator

@arizon-dread arizon-dread Jul 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's good that you're also interested in adding functionality and patching holes, it gives some potential for the future of the project. I'm also interested in enhancement and bug fixes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gentlemen, thanks a lot for your contributions. I did not react until now simply because I did not notice your open PRs. Sorry for that. Will try to set Github alerts for PRs going forward.

As a next step, can you update and test your changes against the current code base (fixed the database update)?


fmt.Printf(time.Now().Format(time.RFC3339) + " Finished scanning: " + part.FileName() + "\n")
}
default:
Expand All @@ -180,24 +214,8 @@ func scanHandlerBody(w http.ResponseWriter, r *http.Request) {
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)
}
fmt.Fprint(w, respJson)
fmt.Printf(time.Now().Format(time.RFC3339)+" Scan result for plain body: %v\n", s)
}

handleScanResults(response, "body", w)
}

func waitForClamD(port string, times int) {
Expand Down Expand Up @@ -248,6 +266,7 @@ func main() {
fmt.Printf("Connected to clamd on %v\n", opts["CLAMD_PORT"])

http.HandleFunc("/scan", scanHandler)
http.HandleFunc("/scanFile", scanFileHandler)
http.HandleFunc("/scanPath", scanPathHandler)
http.HandleFunc("/scanHandlerBody", scanHandlerBody)
http.HandleFunc("/version", clamversion)
Expand Down