305 lines
8.7 KiB
Go
305 lines
8.7 KiB
Go
// Use of this source code is governed by a
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package go-clamav is go wrapper for libclamav see https://docs.clamav.net/manual/Development/libclamav.html
|
|
package goclamav
|
|
|
|
/*
|
|
#cgo CFLAGS: -g -Wall
|
|
#cgo LDFLAGS: -lclamav
|
|
|
|
#include <clamav.h>
|
|
#include <stdlib.h>
|
|
*/
|
|
import "C"
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"unsafe"
|
|
)
|
|
|
|
// Callback is used to store the interface passed to ScanFileCb. This
|
|
// object is then returned in each ClamAV callback for the duration of the
|
|
// file scan
|
|
type Callback struct {
|
|
sync.Mutex
|
|
nextID uintptr
|
|
cb map[unsafe.Pointer]interface{}
|
|
}
|
|
|
|
var callbacks = Callback{
|
|
cb: map[unsafe.Pointer]interface{}{},
|
|
}
|
|
|
|
func setContext(i interface{}) unsafe.Pointer {
|
|
cptr := C.malloc(1)
|
|
if cptr == nil {
|
|
panic("C malloc")
|
|
}
|
|
|
|
callbacks.Lock()
|
|
defer callbacks.Unlock()
|
|
callbacks.cb[cptr] = i
|
|
|
|
return cptr
|
|
}
|
|
|
|
func findContext(key unsafe.Pointer) interface{} {
|
|
callbacks.Lock()
|
|
defer callbacks.Unlock()
|
|
if v, ok := callbacks.cb[key]; ok {
|
|
return v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func deleteContext(key unsafe.Pointer) error {
|
|
callbacks.Lock()
|
|
defer callbacks.Unlock()
|
|
if _, ok := callbacks.cb[key]; ok {
|
|
delete(callbacks.cb, key)
|
|
C.free(key)
|
|
return nil
|
|
}
|
|
return errors.New("no context to delete")
|
|
}
|
|
|
|
type Clamav struct {
|
|
engine *C.struct_cl_engine
|
|
signo uint
|
|
options *C.struct_cl_scan_options
|
|
}
|
|
|
|
// Init new clamav instance
|
|
func (c *Clamav) Init(options SCAN_OPTIONS) error {
|
|
c.engine = (*C.struct_cl_engine)(C.cl_engine_new())
|
|
|
|
scanOptions := &C.struct_cl_scan_options{
|
|
general: C.uint(options.General),
|
|
heuristic: C.uint(options.Heuristic),
|
|
parse: C.uint(options.Parse),
|
|
mail: C.uint(options.Mail),
|
|
dev: C.uint(options.Dev),
|
|
}
|
|
c.options = scanOptions
|
|
|
|
ret := ErrorCode(C.cl_init(CL_INIT_DEFAULT))
|
|
if ret != CL_SUCCESS {
|
|
err := Strerr(ret)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Use the CvdVerify to verify a database directly:
|
|
// As the comment block explains, this will load-test the database. Be advised
|
|
// that for some larger databases, this may use a fair bit system RAM.
|
|
func (c *Clamav) CvdVerify(path string) error {
|
|
_, err := os.Stat(path)
|
|
existed := !os.IsNotExist(err)
|
|
if !existed {
|
|
err := errors.New(fmt.Sprintf("db %s is not exists!", path))
|
|
return err
|
|
}
|
|
|
|
fp := C.CString(path)
|
|
defer C.free(unsafe.Pointer(fp))
|
|
|
|
ret := ErrorCode(C.cl_cvdverify(fp))
|
|
if ret != CL_SUCCESS {
|
|
err := Strerr(ret)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Load clamav virus database
|
|
func (c *Clamav) LoadDB(path string, dbopts uint) (uint, error) {
|
|
_, err := os.Stat(path)
|
|
existed := !os.IsNotExist(err)
|
|
if !existed {
|
|
err := errors.New(fmt.Sprintf("db %s is not exists!", path))
|
|
return 0, err
|
|
}
|
|
|
|
var signo uint
|
|
fp := C.CString(path)
|
|
defer C.free(unsafe.Pointer(fp))
|
|
|
|
ret := ErrorCode(C.cl_load(fp, (*C.struct_cl_engine)(c.engine), (*C.uint)(unsafe.Pointer(&signo)), C.uint(dbopts)))
|
|
if ret != CL_SUCCESS {
|
|
err := Strerr(ret)
|
|
return 0, err
|
|
}
|
|
|
|
return signo, nil
|
|
}
|
|
|
|
// When all required databases are loaded you should prepare the detection engine by calling CompileEngine
|
|
func (c *Clamav) CompileEngine() error {
|
|
ret := ErrorCode(C.cl_engine_compile((*C.struct_cl_engine)(c.engine)))
|
|
if ret != CL_SUCCESS {
|
|
err := Strerr(ret)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetNum sets a number in the specified field of the engine configuration.
|
|
// Certain fields accept only 32-bit numbers, silently truncating the higher bits
|
|
// of the engine config. See dat.go for more information.
|
|
func (c *Clamav) EngineSetNum(field EngineField, num uint64) error {
|
|
ret := ErrorCode(C.cl_engine_set_num((*C.struct_cl_engine)(c.engine), C.enum_cl_engine_field(field), C.longlong(num)))
|
|
if ret != CL_SUCCESS {
|
|
err := Strerr(ret)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetNum acquires a number from the specified field of the engine configuration. Tests show that
|
|
// the ClamAV library will not overflow 32-bit fields, so a GetNum on a 32-bit field can safely be
|
|
// cast to uint32.
|
|
func (c *Clamav) EngineGetNum(field EngineField) (uint64, error) {
|
|
var ret ErrorCode
|
|
ne := (*C.struct_cl_engine)(c.engine)
|
|
num := uint64(C.cl_engine_get_num(ne, C.enum_cl_engine_field(field), (*C.int)(unsafe.Pointer(&ret))))
|
|
if ret != CL_SUCCESS {
|
|
err := Strerr(ret)
|
|
return num, err
|
|
}
|
|
|
|
return num, nil
|
|
}
|
|
|
|
// Free the memory allocated to clamav instance, Free should be called
|
|
// when the engine is no longer in use.
|
|
func (c *Clamav) Free() int {
|
|
return int(C.cl_engine_free((*C.struct_cl_engine)(c.engine)))
|
|
}
|
|
|
|
// ScanMapCb scans custom data
|
|
func (c *Clamav) ScanMapCB(fmap *Fmap, fileName string, context interface{}) (uint, string, error) {
|
|
var scanned C.ulong
|
|
var virusName *C.char
|
|
|
|
fn := C.CString(fileName)
|
|
defer C.free(unsafe.Pointer(fn))
|
|
|
|
// find where to store the context in our callback map. we do _not_ pass the context to
|
|
// C directly because aggressive garbage collection will move it around
|
|
ctx := setContext(context)
|
|
// cleanup
|
|
defer deleteContext(ctx)
|
|
|
|
ret := ErrorCode(C.cl_scanmap_callback((*C.cl_fmap_t)(fmap), fn, &virusName, &scanned, (*C.struct_cl_engine)(c.engine), (*C.struct_cl_scan_options)(c.options), unsafe.Pointer(ctx)))
|
|
defer CloseMemory(fmap)
|
|
// clean
|
|
if ret == CL_SUCCESS {
|
|
return 0, "", nil
|
|
}
|
|
// virus
|
|
if ret == CL_VIRUS {
|
|
return uint(scanned), C.GoString(virusName), Strerr(ret)
|
|
}
|
|
// error
|
|
return 0, "", Strerr(ret)
|
|
}
|
|
|
|
// ScanFile scans a single file for viruses using the ClamAV databases. It returns the number of bytes
|
|
// read from the file (if found), the virus name and an error code.
|
|
// If the file is clean, the virus name is empty and the error code is nil,but if the file is insecure, the
|
|
// error code is "Virus(es) detected" and virus name is the matching rule.
|
|
func (c *Clamav) ScanFile(path string) (uint, string, error) {
|
|
fp := C.CString(path)
|
|
defer C.free(unsafe.Pointer(fp))
|
|
|
|
var virusName *C.char
|
|
var scanned C.ulong
|
|
|
|
ret := ErrorCode(C.cl_scanfile(fp, &virusName, &scanned, (*C.struct_cl_engine)(c.engine), (*C.struct_cl_scan_options)(c.options)))
|
|
// clean
|
|
if ret == CL_SUCCESS {
|
|
return uint(scanned), "", nil
|
|
}
|
|
// virus
|
|
if ret == CL_VIRUS {
|
|
return uint(scanned), C.GoString(virusName), Strerr(ret)
|
|
}
|
|
// error
|
|
return 0, "", Strerr(ret)
|
|
}
|
|
|
|
// ScanFileCb scans a single file for viruses using the ClamAV databases and using callbacks from
|
|
// ClamAV to read/resolve file data. The callbacks can be used to scan files in memory, to scan multiple
|
|
// files inside archives, etc. The function returns the number of bytes
|
|
// read from the file (if found), the virus name and an error code.
|
|
// If the file is clean, the virus name is empty and the error code is nil,but if the file is insecure, the
|
|
// error code is "Virus(es) detected" and virus name is the matching rule.
|
|
// The context argument will be sent back to the callbacks, so effort must be made to retain it
|
|
// throughout the execution of the scan from garbage collection
|
|
func (c *Clamav) ScanFileCB(path string, context interface{}) (uint, string, error) {
|
|
fp := C.CString(path)
|
|
defer C.free(unsafe.Pointer(fp))
|
|
|
|
// find where to store the context in our callback map. we do _not_ pass the context to
|
|
// C directly because aggressive garbage collection will move it around
|
|
ctx := setContext(context)
|
|
// cleanup
|
|
defer deleteContext(ctx)
|
|
|
|
var virusName *C.char
|
|
var scanned C.ulong
|
|
|
|
ret := ErrorCode(C.cl_scanfile_callback(fp, &virusName, &scanned, (*C.struct_cl_engine)(c.engine), (*C.struct_cl_scan_options)(c.options), ctx))
|
|
// clean
|
|
if ret == CL_SUCCESS {
|
|
return 0, "", nil
|
|
}
|
|
// virus
|
|
if ret == CL_VIRUS {
|
|
return uint(scanned), C.GoString(virusName), Strerr(ret)
|
|
}
|
|
// error
|
|
return 0, "", Strerr(ret)
|
|
}
|
|
|
|
// ScanDesc scans a file descriptor for viruses using the ClamAV databases. It returns the number of bytes
|
|
// read from the file (if found), the virus name and an error code.
|
|
// If the file is clean, the virus name is empty and the error code is nil,but if the file is insecure, the
|
|
// error code is "Virus(es) detected" and virus name is the matching rule.
|
|
func (c *Clamav) ScanDesc(desc int32, fileName string) (uint, string, error) {
|
|
var scanned C.ulong
|
|
var virusName *C.char
|
|
|
|
fn := C.CString(fileName)
|
|
defer C.free(unsafe.Pointer(fn))
|
|
|
|
ret := ErrorCode(C.cl_scandesc(C.int(desc), fn, &virusName, &scanned, (*C.struct_cl_engine)(c.engine), (*C.struct_cl_scan_options)(c.options)))
|
|
// clean
|
|
if ret == CL_SUCCESS {
|
|
return 0, "", nil
|
|
}
|
|
// virus
|
|
if ret == CL_VIRUS {
|
|
return uint(scanned), C.GoString(virusName), Strerr(ret)
|
|
}
|
|
// error
|
|
return 0, "", Strerr(ret)
|
|
}
|
|
|
|
// OpenMemory creates an object from the given memory that can be scanned using ScanMapCb
|
|
func OpenMemory(start []byte) *Fmap {
|
|
return (*Fmap)(C.cl_fmap_open_memory(unsafe.Pointer(&start[0]), C.size_t(len(start))))
|
|
}
|
|
|
|
// CloseMemory destroys the fmap associated with an in-memory object
|
|
func CloseMemory(f *Fmap) {
|
|
C.cl_fmap_close((*C.cl_fmap_t)(f))
|
|
}
|