Skip to content

bradsacks99/scancan

Repository files navigation

ScanCan

An HTTP API for a containerized ClamAV

ScanCan provides a light weight RESTful API for ClamAV (https://docs.clamav.net/) in a container.

Building the can

ClamAV Virus defininitions on a mount (recommended)

docker build  -t scancan .
docker rm scancan;docker run -it --volume=/pathtoclamvirusdefs:/opt/clamav --volume=/pathtoamountyouwanttoscan:/opt/files -p portyouwant:8080 --name scancan scancan

You should then update the virus definitions on the mount using freshclam (https://docs.clamav.net/manual/Usage/SignatureManagement.html#freshclam). ClamAV will pick up the new definitions automatically.

Running freshcalm on a cron in the container (not recommended)

docker build --build-arg basev=cron -t scancan .
docker rm scancan;docker run -it --volume=/pathtoamountyouwanttoscan:/opt/files -p portyouwant:8080 --name scancan scancan

Note there are issues with this approach:

  1. It's not well tested
  2. If you are running multiple containers you'll get blocked from downloading at the same time.

Benchmarks

Please take these results for what they are worth...not much.

Running Apache Bench on a local container:

  • Concurrency: 100
  • Requests: 10000
  • Requests scanned a 10 KB zip file on a mounted volume.

Results:

Concurrency Level:      100
Time taken for tests:   17.318 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      2000000 bytes
Total body sent:        2180000
HTML transferred:       560000 bytes
Requests per second:    577.43 [#/sec] (mean)
Time per request:       173.180 [ms] (mean)
Time per request:       1.732 [ms] (mean, across all concurrent requests)
Transfer rate:          112.78 [Kbytes/sec] received
                        122.93 kb/s sent
                        235.71 kb/s total

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   0.5      0       6
Processing:    12  172  13.6    169     242
Waiting:        3  162  13.7    160     236
Total:         12  173  13.5    170     243
WARNING: The median and mean for the initial connection time are not within a normal deviation
        These results are probably not that reliable.

Percentage of the requests served within a certain time (ms)
  50%    170
  66%    173
  75%    177
  80%    181
  90%    191
  95%    199
  98%    209
  99%    218
 100%    243 (longest request)

Usage

ClamAV API documentation is available on:

http://host:hostport/docs

ScanCan v0.1.0

Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu.

Virus Scanning API for ClamAV

Default

GET health

Code samples

# You can also use wget
curl -X GET /health \
  -H 'Accept: application/json'
GET /health HTTP/1.1

Accept: application/json
const headers = {
  'Accept':'application/json'
};

fetch('/health',
{
  method: 'GET',

  headers: headers
})
.then(function(res) {
    return res.json();
}).then(function(body) {
    console.log(body);
});
require 'rest-client'
require 'json'

headers = {
  'Accept' => 'application/json'
}

result = RestClient.get '/health',
  params: {
  }, headers: headers

p JSON.parse(result)
import requests
headers = {
  'Accept': 'application/json'
}

r = requests.get('/health', headers = headers)

print(r.json())
<?php

require 'vendor/autoload.php';

$headers = array(
    'Accept' => 'application/json',
);

$client = new \GuzzleHttp\Client();

// Define array of request body.
$request_body = array();

try {
    $response = $client->request('GET','/health', array(
        'headers' => $headers,
        'json' => $request_body,
       )
    );
    print_r($response->getBody()->getContents());
 }
 catch (\GuzzleHttp\Exception\BadResponseException $e) {
    // handle exception or api errors.
    print_r($e->getMessage());
 }

 // ...
URL obj = new URL("/health");
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("GET");
int responseCode = con.getResponseCode();
BufferedReader in = new BufferedReader(
    new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
    response.append(inputLine);
}
in.close();
System.out.println(response.toString());
package main

import (
       "bytes"
       "net/http"
)

func main() {

    headers := map[string][]string{
        "Accept": []string{"application/json"},
    }

    data := bytes.NewBuffer([]byte{jsonReq})
    req, err := http.NewRequest("GET", "/health", data)
    req.Header = headers

    client := &http.Client{}
    resp, err := client.Do(req)
    // ...
}

GET /health

Health

GET /health: determine the health of ScanCan Returns: result (object)

Example responses

200 Response

null

Responses

Status Meaning Description Schema
200 OK Successful Response Inline

Response Schema

This operation does not require authentication

POST scanpath

Code samples

# You can also use wget
curl -X POST /scanpath/{path} \
  -H 'Accept: application/json'
POST /scanpath/{path} HTTP/1.1

Accept: application/json
const headers = {
  'Accept':'application/json'
};

fetch('/scanpath/{path}',
{
  method: 'POST',

  headers: headers
})
.then(function(res) {
    return res.json();
}).then(function(body) {
    console.log(body);
});
require 'rest-client'
require 'json'

headers = {
  'Accept' => 'application/json'
}

result = RestClient.post '/scanpath/{path}',
  params: {
  }, headers: headers

p JSON.parse(result)
import requests
headers = {
  'Accept': 'application/json'
}

r = requests.post('/scanpath/{path}', headers = headers)

print(r.json())
<?php

require 'vendor/autoload.php';

$headers = array(
    'Accept' => 'application/json',
);

$client = new \GuzzleHttp\Client();

// Define array of request body.
$request_body = array();

try {
    $response = $client->request('POST','/scanpath/{path}', array(
        'headers' => $headers,
        'json' => $request_body,
       )
    );
    print_r($response->getBody()->getContents());
 }
 catch (\GuzzleHttp\Exception\BadResponseException $e) {
    // handle exception or api errors.
    print_r($e->getMessage());
 }

 // ...
URL obj = new URL("/scanpath/{path}");
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("POST");
int responseCode = con.getResponseCode();
BufferedReader in = new BufferedReader(
    new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
    response.append(inputLine);
}
in.close();
System.out.println(response.toString());
package main

import (
       "bytes"
       "net/http"
)

func main() {

    headers := map[string][]string{
        "Accept": []string{"application/json"},
    }

    data := bytes.NewBuffer([]byte{jsonReq})
    req, err := http.NewRequest("POST", "/scanpath/{path}", data)
    req.Header = headers

    client := &http.Client{}
    resp, err := client.Do(req)
    // ...
}

POST /scanpath/{path}

Scan Path

POST /scanpath: scan a mounted path with ClamAV Parameters: path (str): a path Returns: result (Object)

Parameters

Name In Type Required Description
path path string true none

Example responses

200 Response

null

Responses

Status Meaning Description Schema
200 OK Successful Response Inline
422 Unprocessable Entity Validation Error HTTPValidationError

Response Schema

This operation does not require authentication

GET scanurl

Code samples

# You can also use wget
curl -X GET /scanurl/?url=string \
  -H 'Accept: application/json'
GET /scanurl/?url=string HTTP/1.1

Accept: application/json
const headers = {
  'Accept':'application/json'
};

fetch('/scanurl/?url=string',
{
  method: 'GET',

  headers: headers
})
.then(function(res) {
    return res.json();
}).then(function(body) {
    console.log(body);
});
require 'rest-client'
require 'json'

headers = {
  'Accept' => 'application/json'
}

result = RestClient.get '/scanurl/',
  params: {
  'url' => 'string'
}, headers: headers

p JSON.parse(result)
import requests
headers = {
  'Accept': 'application/json'
}

r = requests.get('/scanurl/', params={
  'url': 'string'
}, headers = headers)

print(r.json())
<?php

require 'vendor/autoload.php';

$headers = array(
    'Accept' => 'application/json',
);

$client = new \GuzzleHttp\Client();

// Define array of request body.
$request_body = array();

try {
    $response = $client->request('GET','/scanurl/', array(
        'headers' => $headers,
        'json' => $request_body,
       )
    );
    print_r($response->getBody()->getContents());
 }
 catch (\GuzzleHttp\Exception\BadResponseException $e) {
    // handle exception or api errors.
    print_r($e->getMessage());
 }

 // ...
URL obj = new URL("/scanurl/?url=string");
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("GET");
int responseCode = con.getResponseCode();
BufferedReader in = new BufferedReader(
    new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
    response.append(inputLine);
}
in.close();
System.out.println(response.toString());
package main

import (
       "bytes"
       "net/http"
)

func main() {

    headers := map[string][]string{
        "Accept": []string{"application/json"},
    }

    data := bytes.NewBuffer([]byte{jsonReq})
    req, err := http.NewRequest("GET", "/scanurl/", data)
    req.Header = headers

    client := &http.Client{}
    resp, err := client.Do(req)
    // ...
}

GET /scanurl/

Scan Url

GET /scanurl: scan a url with ClamAV Parameters: url (str): a url Returns: result (Object)

Parameters

Name In Type Required Description
url query string true none

Example responses

200 Response

null

Responses

Status Meaning Description Schema
200 OK Successful Response Inline
422 Unprocessable Entity Validation Error HTTPValidationError

Response Schema

This operation does not require authentication

POST scancont

Code samples

# You can also use wget
curl -X POST /contscan/{path} \
  -H 'Accept: application/json'
POST /contscan/{path} HTTP/1.1

Accept: application/json
const headers = {
  'Accept':'application/json'
};

fetch('/contscan/{path}',
{
  method: 'POST',

  headers: headers
})
.then(function(res) {
    return res.json();
}).then(function(body) {
    console.log(body);
});
require 'rest-client'
require 'json'

headers = {
  'Accept' => 'application/json'
}

result = RestClient.post '/contscan/{path}',
  params: {
  }, headers: headers

p JSON.parse(result)
import requests
headers = {
  'Accept': 'application/json'
}

r = requests.post('/contscan/{path}', headers = headers)

print(r.json())
<?php

require 'vendor/autoload.php';

$headers = array(
    'Accept' => 'application/json',
);

$client = new \GuzzleHttp\Client();

// Define array of request body.
$request_body = array();

try {
    $response = $client->request('POST','/contscan/{path}', array(
        'headers' => $headers,
        'json' => $request_body,
       )
    );
    print_r($response->getBody()->getContents());
 }
 catch (\GuzzleHttp\Exception\BadResponseException $e) {
    // handle exception or api errors.
    print_r($e->getMessage());
 }

 // ...
URL obj = new URL("/contscan/{path}");
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("POST");
int responseCode = con.getResponseCode();
BufferedReader in = new BufferedReader(
    new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
    response.append(inputLine);
}
in.close();
System.out.println(response.toString());
package main

import (
       "bytes"
       "net/http"
)

func main() {

    headers := map[string][]string{
        "Accept": []string{"application/json"},
    }

    data := bytes.NewBuffer([]byte{jsonReq})
    req, err := http.NewRequest("POST", "/contscan/{path}", data)
    req.Header = headers

    client := &http.Client{}
    resp, err := client.Do(req)
    // ...
}

POST /contscan/{path}

Cont Scan

POST /contscan: scan a mounted path with ClamAV, continue if found Parameters: path (str): a path Returns: result (Object)

Parameters

Name In Type Required Description
path path string true none

Example responses

200 Response

null

Responses

Status Meaning Description Schema
200 OK Successful Response Inline
422 Unprocessable Entity Validation Error HTTPValidationError

Response Schema

This operation does not require authentication

POST scanfile (uploaded file)

Code samples

# You can also use wget
curl -X POST /scanfile \
  -H 'Content-Type: multipart/form-data' \
  -H 'Accept: application/json'
POST /scanfile HTTP/1.1

Content-Type: multipart/form-data
Accept: application/json
const inputBody = '{
  "file": "string"
}';
const headers = {
  'Content-Type':'multipart/form-data',
  'Accept':'application/json'
};

fetch('/scanfile',
{
  method: 'POST',
  body: inputBody,
  headers: headers
})
.then(function(res) {
    return res.json();
}).then(function(body) {
    console.log(body);
});
require 'rest-client'
require 'json'

headers = {
  'Content-Type' => 'multipart/form-data',
  'Accept' => 'application/json'
}

result = RestClient.post '/scanfile',
  params: {
  }, headers: headers

p JSON.parse(result)
import requests
headers = {
  'Content-Type': 'multipart/form-data',
  'Accept': 'application/json'
}

r = requests.post('/scanfile', headers = headers)

print(r.json())
<?php

require 'vendor/autoload.php';

$headers = array(
    'Content-Type' => 'multipart/form-data',
    'Accept' => 'application/json',
);

$client = new \GuzzleHttp\Client();

// Define array of request body.
$request_body = array();

try {
    $response = $client->request('POST','/scanfile', array(
        'headers' => $headers,
        'json' => $request_body,
       )
    );
    print_r($response->getBody()->getContents());
 }
 catch (\GuzzleHttp\Exception\BadResponseException $e) {
    // handle exception or api errors.
    print_r($e->getMessage());
 }

 // ...
URL obj = new URL("/scanfile");
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("POST");
int responseCode = con.getResponseCode();
BufferedReader in = new BufferedReader(
    new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
    response.append(inputLine);
}
in.close();
System.out.println(response.toString());
package main

import (
       "bytes"
       "net/http"
)

func main() {

    headers := map[string][]string{
        "Content-Type": []string{"multipart/form-data"},
        "Accept": []string{"application/json"},
    }

    data := bytes.NewBuffer([]byte{jsonReq})
    req, err := http.NewRequest("POST", "/scanfile", data)
    req.Header = headers

    client := &http.Client{}
    resp, err := client.Do(req)
    // ...
}

POST /scanfile

Scan Upload File

POST /scanfile: scan a file stream with ClamAV Parameters: file (bytes): an uploaded file Returns: result (Object)

Body parameter

file: string

Parameters

Name In Type Required Description
body body Body_scan_upload_file_scanfile_post true none

Example responses

200 Response

null

Responses

Status Meaning Description Schema
200 OK Successful Response Inline
422 Unprocessable Entity Validation Error HTTPValidationError

Response Schema

This operation does not require authentication

GET license

Code samples

# You can also use wget
curl -X GET /license \
  -H 'Accept: text/plain'
GET /license HTTP/1.1

Accept: text/plain
const headers = {
  'Accept':'text/plain'
};

fetch('/license',
{
  method: 'GET',

  headers: headers
})
.then(function(res) {
    return res.json();
}).then(function(body) {
    console.log(body);
});
require 'rest-client'
require 'json'

headers = {
  'Accept' => 'text/plain'
}

result = RestClient.get '/license',
  params: {
  }, headers: headers

p JSON.parse(result)
import requests
headers = {
  'Accept': 'text/plain'
}

r = requests.get('/license', headers = headers)

print(r.json())
<?php

require 'vendor/autoload.php';

$headers = array(
    'Accept' => 'text/plain',
);

$client = new \GuzzleHttp\Client();

// Define array of request body.
$request_body = array();

try {
    $response = $client->request('GET','/license', array(
        'headers' => $headers,
        'json' => $request_body,
       )
    );
    print_r($response->getBody()->getContents());
 }
 catch (\GuzzleHttp\Exception\BadResponseException $e) {
    // handle exception or api errors.
    print_r($e->getMessage());
 }

 // ...
URL obj = new URL("/license");
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("GET");
int responseCode = con.getResponseCode();
BufferedReader in = new BufferedReader(
    new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
    response.append(inputLine);
}
in.close();
System.out.println(response.toString());
package main

import (
       "bytes"
       "net/http"
)

func main() {

    headers := map[string][]string{
        "Accept": []string{"text/plain"},
    }

    data := bytes.NewBuffer([]byte{jsonReq})
    req, err := http.NewRequest("GET", "/license", data)
    req.Header = headers

    client := &http.Client{}
    resp, err := client.Do(req)
    // ...
}

GET /license

Show License

GET /license: view the ScanCan license Returns: license (string)

Example responses

200 Response

"string"

Responses

Status Meaning Description Schema
200 OK Successful Response string
This operation does not require authentication

Schemas

Body_scan_upload_file_scanfile_post

{
  "file": "string"
}

Body_scan_upload_file_scanfile_post

Properties

Name Type Required Restrictions Description
file string(binary) true none none

HTTPValidationError

{
  "detail": [
    {
      "loc": [
        "string"
      ],
      "msg": "string",
      "type": "string"
    }
  ]
}

HTTPValidationError

Properties

Name Type Required Restrictions Description
detail [ValidationError] false none none

ValidationError

{
  "loc": [
    "string"
  ],
  "msg": "string",
  "type": "string"
}

ValidationError

Properties

Name Type Required Restrictions Description
loc [anyOf] true none none

anyOf

Name Type Required Restrictions Description
» anonymous string false none none

or

Name Type Required Restrictions Description
» anonymous integer false none none

continued

Name Type Required Restrictions Description
msg string true none none
type string true none none